Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions custom-elements-manifest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { customJSDocTagsPlugin } from 'cem-plugin-custom-jsdoc-tags';
import forgeMemberDenyListPlugin from './plugins/cem/member-deny-list.mjs';
import forgePublicMemberPrivacyPlugin from './plugins/cem/public-member-privacy.mjs';
import forgeTypePathsPlugin from './plugins/cem/type-paths.mjs';
import forgeReactTypesPlugin from './plugins/cem/react-types.mjs';
import forgeBranchNamePlugin from './plugins/cem/branch-name.mjs';
import forgeFilterIgnored from './plugins/cem/filter-ignored.mjs';

Expand All @@ -17,6 +18,9 @@ export default {
forgeMemberDenyListPlugin(),
forgePublicMemberPrivacyPlugin(),
forgeTypePathsPlugin(),
forgeReactTypesPlugin({
outDir: path.resolve('./dist/cem')
}),
customElementVsCodePlugin({
hideLogs: true,
outdir: path.resolve('dist/cem')
Expand Down
5 changes: 5 additions & 0 deletions forge.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"pattern": "./dist/cem/vscode.*.json",
"root": "./dist/cem/",
"output": "dist"
},
{
"pattern": "./dist/cem/react-types.d.ts",
"root": "./dist/cem/",
"output": "types"
}
]
},
Expand Down
110 changes: 110 additions & 0 deletions plugins/cem/react-types.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import fs from 'fs';
import path from 'path';
import prettier from 'prettier';
import { getCustomElements, getPublicMembers } from './utils.mjs';

/**
* Types that should be converted to `any` in the generated typings file.
*/
const IGNORED_ANY_TYPES = ['TValue'];

/**
* This plugin generates a .d.ts file for ambient TypeScript type definitions for all Forge custom elements on `JSX.IntrinsicElements`.
*/
export default function forgeJSXTypesPlugin({ outDir = './', fileName = 'react-types.d.ts' } = {}) {
return {
name: 'FORGE - JSX-TYPES',
async packageLinkPhase({ customElementsManifest }) {
// Get all custom element declarations from the manifest
const components = getCustomElements(customElementsManifest);

// Capture all of the available public types from the manifest that could exist as public component API types
const allTypes = Object.keys(customElementsManifest.forgeTypes);

// Create the typings file contents
const contents = createTypingsFromComponents(components, allTypes);

// Ensure the output directory exists
if (outDir !== './' && !fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}

// Write the formatted file to the output directory
const outputPath = path.join(outDir, fileName);
fs.writeFileSync(outputPath, await prettier.format(contents, { parser: 'typescript', printWidth: 120, singleQuote: true }), 'utf-8');
}
};
}

function createTypingsFromComponents(components, allTypes) {
const REFERENCED_TYPES_PLACEHOLDER = '<!-- FORGE_TYPE_IMPORTS_PLACEHOLDER -->';

// prettier-ignore
const declaration = `
import React from 'react';

${REFERENCED_TYPES_PLACEHOLDER}

type ReactHTMLElementProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;

${components.map(component => {
return `
type IForge${component.name}Props = {
${getComponentPropertiesAndAttributes(component)}
${getComponentEvents(component)}
}
`;
}).join('\n')}

/**
* To use Forge custom elements in JSX, you need to augment the JSX.IntrinsicElements interface with the custom element tags.
*
* Usage:
* \`\`\`ts
* import type { ForgeCustomElements } from '@tylertech/forge/types/react-types';
*
* declare global {
* namespace JSX {
* interface IntrinsicElements extends ForgeCustomElements {}
* }
* }
* \`\`\`
*/
export type ForgeCustomElements = {
${components.map(component => {
return `'${component.tagName}': Partial<IForge${component.name}Props | ReactHTMLElementProps>;`;
}).join('\n')}
}
`;

// Create an import statement for any of the Forge global types that are referenced in the component declarations
const referencedTypes = allTypes.filter(type => declaration.match(new RegExp(`\\b${type}\\b`)));
const typeImport = `
import type {
${referencedTypes.map(type => type).join(',\n')}
} from '@tylertech/forge';
`;
return declaration.replace(REFERENCED_TYPES_PLACEHOLDER, typeImport);
}

function getComponentPropertiesAndAttributes(component) {
const properties = getPublicMembers(component);
const attributes = (component.attributes ?? []).filter(attr => !properties.find(prop => prop.name === attr.name));
const uniquePropsAndAttrs = [...properties, ...attributes];
return uniquePropsAndAttrs
.map(attr => {
const type = attr.type?.text ?? 'string';
return `'${attr.name}'?: ${IGNORED_ANY_TYPES.includes(type) ? 'any' : type};`;
})
.join('\n');
}

function getComponentEvents(component) {
return (
component.events
?.map(event => {
return `'on${event.name}'?: (e: ${event.type?.text ?? 'CustomEvent'}) => void;`;
})
.join('\n') ?? ''
);
}
23 changes: 23 additions & 0 deletions plugins/cem/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Get all custom element declarations from a custom elements manifest.
*/
export function getCustomElements(customElementsManifest) {
return customElementsManifest.modules.flatMap(module => module.declarations.filter(declaration => declaration.customElement && !!declaration.tagName));
}

/**
* Get all public members of a custom element declaration.
*/
export function getPublicMembers(declaration) {
return (
declaration.members?.filter(
member =>
member.kind === 'field' &&
member.privacy !== 'private' &&
member.privacy !== 'protected' &&
!member.static &&
!member.attribute &&
!member.name.startsWith('#')
) ?? []
);
}
2 changes: 1 addition & 1 deletion src/lib/app-bar/app-bar/app-bar-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ export const APP_BAR_CONSTANTS = {
};

export type AppBarElevation = 'none' | 'raised';
export type AppBarTheme = 'white' | '';
export type AppBarTheme = 'white' | 'custom' | '';
2 changes: 1 addition & 1 deletion src/lib/overlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ declare global {
* @attribute {string} boundary - The id of the element to use as the boundary for the overlay.
* @attribute {string} fallback-placements - The fallback placements to use when the overlay cannot be placed in the desired placement. Should be a comma separated list of placements.
*
* @event {CustomEvent<OverlayToggleEventData>} forge-overlay-light-dismiss - Dispatches when the overlay is light dismissed via the escape key or clicking outside the overlay.
* @event {CustomEvent<OverlayLightDismissEventData>} forge-overlay-light-dismiss - Dispatches when the overlay is light dismissed via the escape key or clicking outside the overlay.
*
* @cssproperty --forge-overlay-position - The `position` of the overlay.
* @cssproperty --forge-overlay-z-index - The `z-index` of the overlay. Defaults to the `popup` range.
Expand Down
19 changes: 18 additions & 1 deletion src/stories/frameworks/React.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,27 @@ import { Meta } from '@storybook/blocks';
Direct support for custom elements in React has been a requested feature for long time. As you can see on
[Custom Elements Everywhere](https://custom-elements-everywhere.com/#react) React scores quite low.

> As of May 2024, React 19 is in beta and now [better supports custom elements](https://custom-elements-everywhere.com/#react-beta) natively.
> As of May 2024, React v19 is in beta and now [better supports custom elements](https://custom-elements-everywhere.com/#react-beta) natively.
>
> This means that you can use Forge components directly in React more seamlessly without any additional libraries! 🎉

## Ambient Typings

When using Forge components in React, you can reference ambient typings for the `<forge-*>` custom elements. This is useful when using TypeScript
to avoid errors when using the components in JSX.

Forge provides ambient typings that you can use in your React application. To reference these typings, add the following code to your project:

```ts
import type { ForgeCustomElements } from '@tylertech/forge/types/react-types';

declare global {
namespace JSX {
interface IntrinsicElements extends ForgeCustomElements {}
}
}
```

## The Problem

React doesn't pass data through the JavaScript API on HTML elements. This means that any Web Components created
Expand Down