diff --git a/packages/web-components/README.md b/packages/web-components/README.md index 18686453cc9860..7b572f25fe9d84 100644 --- a/packages/web-components/README.md +++ b/packages/web-components/README.md @@ -76,6 +76,16 @@ import { ButtonDefinition, FluentDesignSystem } from '@fluentui/web-components'; ButtonDefinition.define(FluentDesignSystem.registry); ``` +## Developer Experience + +For convenience we have included a [CEM (custom elements manifest)](https://github.com/webcomponents/custom-elements-manifest). + +```js +import CEM from '@fluentui/custom-elements.json'; +``` + +We have also included an [HTML custom data file for VS Code](./vs-code.md). + ## Development To start the component development environment, run `yarn start`. diff --git a/packages/web-components/custom-elements-manifest.config.js b/packages/web-components/custom-elements-manifest.config.js index 53ccadedfeb428..db7c46f7592d23 100644 --- a/packages/web-components/custom-elements-manifest.config.js +++ b/packages/web-components/custom-elements-manifest.config.js @@ -1,4 +1,5 @@ -import { tagNameFix } from "./custom-elements-manifest.plugins.js"; +import { tagNameFix, typescriptTypeTextSanitize } from "./custom-elements-manifest.plugins.js"; +import { customElementVsCodePlugin } from "custom-element-vs-code-integration"; export default { /** Globs to analyze */ @@ -24,5 +25,11 @@ export default { dev: false, /** Enable special handling for fast */ fast: true, - plugins: [tagNameFix()], + plugins: [ + tagNameFix(), + typescriptTypeTextSanitize(), + customElementVsCodePlugin({ + outdir: "dist" + }) + ], }; diff --git a/packages/web-components/custom-elements-manifest.plugins.js b/packages/web-components/custom-elements-manifest.plugins.js index e5324dbf99c69a..39a67aa245be1d 100644 --- a/packages/web-components/custom-elements-manifest.plugins.js +++ b/packages/web-components/custom-elements-manifest.plugins.js @@ -1,9 +1,89 @@ -const pascalToKebab = (input) => { - return input - .split(/\.?(?=[A-Z])/) - .join('-') - .toLowerCase(); -}; +import { readFileSync } from "fs"; +import path from "path"; +import ts from "typescript"; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const getTagNameFromCommentInDefinitionFile = function (definitionPathName) { + const indexFilePath = path.resolve(__dirname, `./${definitionPathName}`); + let name; + + try { + let sourceFile = ts.createSourceFile( + indexFilePath, + readFileSync(indexFilePath).toString(), + ts.ScriptTarget.ES2015, + /*setParentNodes */ true + ); + + if (Array.isArray(sourceFile.statements)) { + sourceFile.statements.forEach((statement) => { + if (Array.isArray(statement.jsDoc) && statement.jsDoc[0].tags !== undefined) { + statement.jsDoc.forEach((jsDoc) => { + if (Array.isArray(jsDoc.tags)) { + jsDoc.tags.forEach((tag) => { + if (typeof tag.comment === "string" && tag.comment.startsWith("HTML Element:")) { + name = tag.comment.match(/<(.*)>/)[1].replace("\\", ""); + } + }); + } + }) + } + }) + } + } catch (err) { + // do nothing + } + + return name; +} + +const resolveDefinitionFilePath = function(filePathName) { + return filePathName.split("/").map((pathItem) => { + if (pathItem.endsWith(".ts")) { + const splitPathItem = pathItem.split("."); + splitPathItem.splice(1, 0, "definition"); + return splitPathItem.join("."); + } + + return pathItem; + }).join("/"); +} + +const checkIsUnresolvedTypeScriptType = function(type) { + /** + * Due to TypeScript types being PascalCase, and all other default + * types being lowercase, we determine if this is a typescript type by checking + * the first letter. + */ + return type[0] === type[0].toUpperCase() && isNaN(type[0]); +} + +const resolveTypeToValues = function(CEM, type) { + let values = ""; + + CEM.modules.forEach((cemModule) => { + if (cemModule.kind === "javascript-module") { + cemModule.declarations.forEach((declaration) => { + if (declaration.name === type) { + const sanitizedType = declaration.type?.text; + const matches = sanitizedType + .match(/((?:.*):(?:.*))/gm) + .map((match) => { + return match.match(/(?:(?:')(.*)(?:')|(\d+))/)[0]; + }); + values = matches.reduce((accum, match) => { + return `${accum}${accum === "" ? "" : " | "}${match}`; + }, values) + } + }) + } + }); + + return values; +} /** * @return {import('@custom-elements-manifest/analyzer').Plugin} @@ -18,7 +98,68 @@ export function tagNameFix() { customElementsManifest.modules.map((item) => { item.declarations.forEach((declaration) => { if (declaration.customElement) { - declaration.tagName = `fluent-${pascalToKebab(declaration.name)}`; + const name = getTagNameFromCommentInDefinitionFile( + resolveDefinitionFilePath(item.path) + ); + + if (typeof name === "undefined") { + console.error(`no tag name for ${item.path}`); + } else { + declaration.tagName = name; + } + } + }) + }); + }, + }; +} + +/** + * @return {import('@custom-elements-manifest/analyzer').Plugin} + * + * This plugin changes the types to use pipe syntax so that the vscode plugin can + * correctly interpret the possible values, eg. + * from: + * { + * "name": "heading-level", + * "type": { + * "text": "HeadingLevel" + * }, + * "fieldName": "headinglevel" + * } + * + * to: + * { + * "name": "heading-level", + * "type": { + * "text": "1 | 2 | 3 | 4 | 5 | 6" + * }, + * "default": "2", + * "fieldName": "headinglevel" + * }, + */ +export function typescriptTypeTextSanitize() { + return { + name: "typescriptTypeTextSanitize", + packageLinkPhase({customElementsManifest, context}){ + customElementsManifest.modules.map((item) => { + item.declarations.forEach((declaration) => { + if (declaration.customElement && Array.isArray(declaration.attributes)) { + declaration.attributes.forEach((attribute) => { + if (typeof attribute.type?.text === "string") { + const possibleTypes = attribute.type.text.split("|").map((item) => { + return item.trim(); + }).map((possibleType) => { + if (checkIsUnresolvedTypeScriptType(possibleType)) { + return resolveTypeToValues(customElementsManifest, possibleType); + } + + return possibleType; + }).join(" | "); + + attribute.type.text = possibleTypes; + } + }); } }) }); diff --git a/packages/web-components/package.json b/packages/web-components/package.json index e2679cbe80261f..b3dba5069663d5 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -23,7 +23,8 @@ "dist/dts/", "dist/esm/", "dist/*.js", - "dist/*.d.ts" + "dist/*.d.ts", + "dist/*.json" ], "exports": { ".": { @@ -248,10 +249,11 @@ "devDependencies": { "@microsoft/fast-element": "2.0.0-beta.26", "@tensile-perf/web-components": "~0.2.0", - "@custom-elements-manifest/analyzer": "^0.10.2", + "@custom-elements-manifest/analyzer": "9.8.0", "@types/web": "^0.0.142", "@storybook/html": "6.5.15", - "chromedriver": "^125.0.0" + "chromedriver": "^125.0.0", + "custom-element-vs-code-integration": "^1.4.0" }, "dependencies": { "@microsoft/fast-web-utilities": "^6.0.0", diff --git a/packages/web-components/src/avatar/avatar.definition.ts b/packages/web-components/src/avatar/avatar.definition.ts index 6f076756e58606..5882dddd6aa3a9 100644 --- a/packages/web-components/src/avatar/avatar.definition.ts +++ b/packages/web-components/src/avatar/avatar.definition.ts @@ -8,7 +8,7 @@ import { template } from './avatar.template.js'; * * @public * @remarks - * HTML Element: \ + * HTML Element: \ */ export const definition = Avatar.compose({ name: `${FluentDesignSystem.prefix}-avatar`, diff --git a/packages/web-components/src/compound-button/compound-button.definition.ts b/packages/web-components/src/compound-button/compound-button.definition.ts index ee2f7922f68972..4e80366c1967b1 100644 --- a/packages/web-components/src/compound-button/compound-button.definition.ts +++ b/packages/web-components/src/compound-button/compound-button.definition.ts @@ -6,7 +6,7 @@ import { template } from './compound-button.template.js'; /** * @public * @remarks - * HTML Element: \ + * HTML Element: \ */ export const definition = CompoundButton.compose({ name: `${FluentDesignSystem.prefix}-compound-button`, diff --git a/packages/web-components/src/divider/divider.options.ts b/packages/web-components/src/divider/divider.options.ts index c537eeab5b92c5..06f40304d8c167 100644 --- a/packages/web-components/src/divider/divider.options.ts +++ b/packages/web-components/src/divider/divider.options.ts @@ -1,4 +1,3 @@ -import { Orientation } from '@microsoft/fast-web-utilities'; import type { ValuesOf } from '../utils/index.js'; /** @@ -27,7 +26,10 @@ export type DividerRole = ValuesOf; * Divider orientation * @public */ -export const DividerOrientation = Orientation; +export const DividerOrientation = { + horizontal: 'horizontal', + vertical: 'vertical', +} as const; /** * The types for Divider orientation diff --git a/packages/web-components/src/drawer-body/drawer-body.definition.ts b/packages/web-components/src/drawer-body/drawer-body.definition.ts index 562c0647ad36cb..99fed79f27509e 100644 --- a/packages/web-components/src/drawer-body/drawer-body.definition.ts +++ b/packages/web-components/src/drawer-body/drawer-body.definition.ts @@ -7,7 +7,7 @@ import { template } from './drawer-body.template.js'; * * @public * @remarks - * HTML Element: + * HTML Element: */ export const definition = DrawerBody.compose({ diff --git a/packages/web-components/src/menu-button/menu-button.definition.ts b/packages/web-components/src/menu-button/menu-button.definition.ts index 4ffae145c2c7fd..dc47ac72816612 100644 --- a/packages/web-components/src/menu-button/menu-button.definition.ts +++ b/packages/web-components/src/menu-button/menu-button.definition.ts @@ -6,7 +6,7 @@ import { template } from './menu-button.template.js'; /** * @public * @remarks - * HTML Element: \ + * HTML Element: \ */ export const definition = MenuButton.compose({ name: `${FluentDesignSystem.prefix}-menu-button`, diff --git a/packages/web-components/src/radio-group/radio-group.options.ts b/packages/web-components/src/radio-group/radio-group.options.ts index d3af40bc749f50..41b70c6a60bda4 100644 --- a/packages/web-components/src/radio-group/radio-group.options.ts +++ b/packages/web-components/src/radio-group/radio-group.options.ts @@ -1,11 +1,13 @@ -import { Orientation } from '@microsoft/fast-web-utilities'; import type { ValuesOf } from '../utils/index.js'; /** * Radio Group orientation * @public */ -export const RadioGroupOrientation = Orientation; +export const RadioGroupOrientation = { + horizontal: 'horizontal', + vertical: 'vertical', +} as const; /** * Types of Radio Group orientation diff --git a/packages/web-components/src/tab-panel/tab-panel.definition.ts b/packages/web-components/src/tab-panel/tab-panel.definition.ts index b94f49a3289680..32038505e39cff 100644 --- a/packages/web-components/src/tab-panel/tab-panel.definition.ts +++ b/packages/web-components/src/tab-panel/tab-panel.definition.ts @@ -3,6 +3,13 @@ import { TabPanel } from './tab-panel.js'; import { template } from './tab-panel.template.js'; import { styles } from './tab-panel.styles.js'; +/** + * The definition for the Fluent Tab Panel component. + * + * @public + * @remarks + * HTML Element: `` + */ export const definition = TabPanel.compose({ name: `${FluentDesignSystem.prefix}-tab-panel`, template, diff --git a/packages/web-components/src/tab/tab.definition.ts b/packages/web-components/src/tab/tab.definition.ts index 16fbae5108492c..e711a543b85a57 100644 --- a/packages/web-components/src/tab/tab.definition.ts +++ b/packages/web-components/src/tab/tab.definition.ts @@ -3,6 +3,13 @@ import { Tab } from './tab.js'; import { template } from './tab.template.js'; import { styles } from './tab.styles.js'; +/** + * The definition for the Fluent Tab component. + * + * @public + * @remarks + * HTML Element: `` + */ export const definition = Tab.compose({ name: `${FluentDesignSystem.prefix}-tab`, template, diff --git a/packages/web-components/src/tabs/tabs.definition.ts b/packages/web-components/src/tabs/tabs.definition.ts index f3b2494308437d..fb3244c943ca8a 100644 --- a/packages/web-components/src/tabs/tabs.definition.ts +++ b/packages/web-components/src/tabs/tabs.definition.ts @@ -3,6 +3,13 @@ import { Tabs } from './tabs.js'; import { template } from './tabs.template.js'; import { styles } from './tabs.styles.js'; +/** + * The definition for the Fluent Tabs component. + * + * @public + * @remarks + * HTML Element: `` + */ export const definition = Tabs.compose({ name: `${FluentDesignSystem.prefix}-tabs`, template, diff --git a/packages/web-components/src/tabs/tabs.options.ts b/packages/web-components/src/tabs/tabs.options.ts index c236c1ec53e677..22cec7fba9929b 100644 --- a/packages/web-components/src/tabs/tabs.options.ts +++ b/packages/web-components/src/tabs/tabs.options.ts @@ -1,4 +1,3 @@ -import { Orientation } from '@microsoft/fast-web-utilities'; import { StartEndOptions } from '../patterns/index.js'; import type { ValuesOf } from '../utils/index.js'; import { Tabs } from './tabs.js'; @@ -28,7 +27,10 @@ export type TabsOptions = StartEndOptions; * The orientation of the component * @public */ -export const TabsOrientation = Orientation; +export const TabsOrientation = { + horizontal: 'horizontal', + vertical: 'vertical', +} as const; /** * The types for the Tabs component diff --git a/packages/web-components/vs-code.md b/packages/web-components/vs-code.md new file mode 100644 index 00000000000000..119b74241781b9 --- /dev/null +++ b/packages/web-components/vs-code.md @@ -0,0 +1,17 @@ +# Using VS Code + +As an additional helper included in the `@fluentui/web-components` package is HTML custom data. + +![VS Code Custom Data user experience](./vscode-custom-html-data.gif) + +To use this, reference the custom data file in your VS Code settings: + +```json +{ + "html.customData": ["../node_modules/@fluentui/web-components/dist/vscode.html-custom-data.json"] +} +``` + +**Note:** The path is relative to the root of the project, not the settings file. + +Once it has been added, you will need to restart VS Code in order for it to register the new components. diff --git a/packages/web-components/vscode-custom-html-data.gif b/packages/web-components/vscode-custom-html-data.gif new file mode 100644 index 00000000000000..9eedf9ed226d90 Binary files /dev/null and b/packages/web-components/vscode-custom-html-data.gif differ diff --git a/yarn.lock b/yarn.lock index c463068fc86746..e34eba730429c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1638,10 +1638,10 @@ resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.3.4.tgz#59691edd031eedc431bda1bdf601257c06289a40" integrity sha512-8vmPV/nIULFDWsnJalQJDqFLC2uTPx6A/ASA2t27QGp+7oXnbWWXCe0uV8xasIH2rGbI/XoB2vmkdP/94WvMrw== -"@custom-elements-manifest/analyzer@^0.10.2": - version "0.10.2" - resolved "https://registry.yarnpkg.com/@custom-elements-manifest/analyzer/-/analyzer-0.10.2.tgz#5f71d02f45c3db691e752ca19e4bee8ea4068ded" - integrity sha512-YkfAfaNGSulnXxyIAHU3K8Z7bYGmIU2MlPvEaQPXnWUaIFMo0p3VEVYvByvENnVCQKPPDyjlkCm73u/zRnRvMA== +"@custom-elements-manifest/analyzer@9.8.0": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@custom-elements-manifest/analyzer/-/analyzer-0.10.3.tgz#3b12957514475b24672ed08f48e007c7e5f018ea" + integrity sha512-e2Ax59vK9sNedmDlPqZS11L54iAlKSjOJuv5etpTy5SygLBW3GcUtocHZm8wO013L0griTPpgWB0tuV7/JXy5A== dependencies: "@custom-elements-manifest/find-dependencies" "^0.0.5" "@github/catalyst" "^1.6.0" @@ -3477,6 +3477,13 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@prettier/sync@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@prettier/sync/-/sync-0.5.2.tgz#f8401e45b667e8d6207015fd03619ea2c2e3e680" + integrity sha512-Yb569su456XNx5BsH/Vyem7xD6g/y9iLmLUzRKM1a/dhU/D7HqqvkAG72znulXlMXztbV0iiu9O5AL8K98TzZQ== + dependencies: + make-synchronized "^0.2.8" + "@react-native/babel-plugin-codegen@0.73.4": version "0.73.4" resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.73.4.tgz#8a2037d5585b41877611498ae66adbf1dddfec1b" @@ -10027,6 +10034,13 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +custom-element-vs-code-integration@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/custom-element-vs-code-integration/-/custom-element-vs-code-integration-1.4.1.tgz#3afb347705a48d0cea3606349c06c795e919c75e" + integrity sha512-aOQpNayEzXHUg7JRo/eIS8aCMiOPLuMwhANj4iFdAz3NnHy5Y0Us7uS/qSeJhYbl+5NddygQvp3is8L0ggxMbQ== + dependencies: + "@prettier/sync" "^0.5.2" + custom-elements-manifest@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz#b35c2129076a1dc9f95d720c6f7b5b71a857274b" @@ -17549,6 +17563,11 @@ make-iterator@^1.0.0: dependencies: kind-of "^6.0.2" +make-synchronized@^0.2.8: + version "0.2.9" + resolved "https://registry.yarnpkg.com/make-synchronized/-/make-synchronized-0.2.9.tgz#edcbe2d8e7aeac8e0f41a0bb25b05cc7a7e2e8e4" + integrity sha512-4wczOs8SLuEdpEvp3vGo83wh8rjJ78UsIk7DIX5fxdfmfMJGog4bQzxfvOwq7Q3yCHLC4jp1urPHIxRS/A93gA== + makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a"