From f76dea1b03697e05562e92eb4d60502ca594b20f Mon Sep 17 00:00:00 2001 From: Burton Smith Date: Thu, 5 Jun 2025 07:32:24 -0400 Subject: [PATCH 1/3] Add JSX types and documentation --- custom-elements-manifest.config.js | 8 + docs/_includes/sidebar.njk | 1 + docs/pages/frameworks/jsx.md | 155 ++++++++++++++++++ docs/pages/frameworks/react.md | 4 + package-lock.json | 14 ++ package.json | 2 + src/components/alert/alert.component.ts | 2 + .../animated-image.component.ts | 4 + .../animation/animation.component.ts | 1 + .../breadcrumb-item.component.ts | 1 + .../breadcrumb/breadcrumb.component.ts | 3 + .../button-group/button-group.component.ts | 2 + src/components/button/button.component.ts | 5 + src/components/carousel/carousel.component.ts | 7 +- src/components/checkbox/checkbox.component.ts | 2 + .../color-picker/color-picker.component.ts | 9 + .../copy-button/copy-button.component.ts | 10 ++ src/components/details/details.component.ts | 9 + src/components/dialog/dialog.component.ts | 11 +- src/components/drawer/drawer.component.ts | 11 +- src/components/dropdown/dropdown.component.ts | 20 ++- .../icon-button/icon-button.component.ts | 7 +- .../image-comparer.component.ts | 4 + src/components/include/include.component.ts | 1 + src/components/input/input.component.ts | 10 +- .../menu-item/menu-item.component.ts | 7 + src/components/menu/menu.component.ts | 1 + .../mutation-observer.component.ts | 2 + src/components/option/option.component.ts | 15 +- .../progress-ring/progress-ring.component.ts | 2 + src/components/qr-code/qr-code.component.ts | 2 + .../radio-button/radio-button.component.ts | 8 +- .../radio-group/radio-group.component.ts | 9 +- src/components/radio/radio.component.ts | 3 + src/components/range/range.component.ts | 12 +- 35 files changed, 341 insertions(+), 23 deletions(-) create mode 100644 docs/pages/frameworks/jsx.md diff --git a/custom-elements-manifest.config.js b/custom-elements-manifest.config.js index cb8c3ae4..bc25485f 100644 --- a/custom-elements-manifest.config.js +++ b/custom-elements-manifest.config.js @@ -2,6 +2,7 @@ import * as path from 'path'; import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration'; import { customElementVsCodePlugin } from 'custom-element-vs-code-integration'; import { customElementVuejsPlugin } from 'custom-element-vuejs-integration'; +import { jsxTypesPlugin } from "@wc-toolkit/jsx-types"; import { parse } from 'comment-parser'; import { pascalCase } from 'pascal-case'; import commandLineArgs from 'command-line-args'; @@ -225,6 +226,13 @@ export default { outdir: './dist/types/vue', fileName: 'index.d.ts', componentTypePath: (_, tag) => `../../components/${tag.replace('sl-', '')}/${tag.replace('sl-', '')}.component.js` + }), + jsxTypesPlugin({ + outdir: './dist/types/jsx', + fileName: 'index.d.ts', + allowUnknownProps: true, + defaultExport: true, + componentTypePath: (_, tag) => `../../components/${tag?.replace('sl-', '')}/${tag?.replace('sl-', '')}.component.js` }) ] }; diff --git a/docs/_includes/sidebar.njk b/docs/_includes/sidebar.njk index df0958bb..5e53dcf8 100644 --- a/docs/_includes/sidebar.njk +++ b/docs/_includes/sidebar.njk @@ -18,6 +18,7 @@
  • Vue
  • Angular
  • Svelte
  • +
  • JSX
  • diff --git a/docs/pages/frameworks/jsx.md b/docs/pages/frameworks/jsx.md new file mode 100644 index 00000000..955a0b89 --- /dev/null +++ b/docs/pages/frameworks/jsx.md @@ -0,0 +1,155 @@ +--- +meta: + title: JSX + description: Tips for using Shoelace in JSX. +--- + +# JSX Integration + +Shoelace provides comprehensive JSX/TSX support with full TypeScript definitions. This makes it easy to use Shoelace components in any JSX-based framework like React (19+), Preact, or Solid.js. + +## Installation + +First, install Shoelace: + +```bash +npm install @shoelace-style/shoelace +``` + +## TypeScript Setup + +In order for teams to take advantage of this, all they need to do is import the types in their project. There are two ways to configure the JSX types: + +### Add Types to Config + +Add the types to your tsconfig.json: + +```json +{ + "compilerOptions": { + "types": ["@shoelace-style/shoelace/dist/types/jsx"] + } +} +``` + +### Manual Type Extension + +Alternatively, you can manually extend the JSX namespace in your own type definition file: + +```tsx +// types/jsx.d.ts +import type { CustomElements } from '@shoelace-style/shoelace/dist/types/jsx'; + +// The module name is typically something like 'react', 'preact' +// or whatever the package name is that provides JSX support. +declare module 'react' { + namespace JSX { + interface IntrinsicElements extends CustomElements {} + } +} +``` + +## Basic Usage + +Import the components you need and use them like any other JSX element: + +```tsx +import '@shoelace-style/shoelace/dist/themes/light.css'; +import '@shoelace-style/shoelace/dist/components/button/button.js'; +import '@shoelace-style/shoelace/dist/components/card/card.js'; +import '@shoelace-style/shoelace/dist/components/icon/icon.js'; + +function App() { + const handleClick = () => { + console.log('Button clicked!'); + }; + + return ( + +

    Welcome to Shoelace

    +

    This is a Shoelace card component used in JSX.

    + + + Star + +
    + ); +} +``` + +## Event Handling + +Shoelace components emit custom events. The JSX types provide properly typed event handlers: + +```tsx +import '@shoelace-style/shoelace/dist/components/input/input.js'; +import '@shoelace-style/shoelace/dist/components/select/select.js'; + +function FormComponent() { + const handleInput = (event: Event) => { + const input = event.target as HTMLInputElement; + console.log('Input value:', input.value); + }; + + const handleChange = (event: CustomEvent) => { + console.log('Selection changed:', event.detail.item.value); + }; + + return ( +
    + + + + Option 1 + Option 2 + Option 3 + +
    + ); +} +``` + +## Slots + +Use the `slot` attribute to place content in named slots: + +```tsx +import '@shoelace-style/shoelace/dist/components/dialog/dialog.js'; +import '@shoelace-style/shoelace/dist/components/button/button.js'; + +function DialogExample() { + return ( + +

    This content goes in the default slot.

    + + + Cancel + + + Confirm + +
    + ); +} +``` + +## Type Safety + +The JSX types provide full IntelliSense support and type checking: + +```tsx +// ✅ Valid - all properties are properly typed + + +// ❌ Invalid - TypeScript will catch these errors + +``` diff --git a/docs/pages/frameworks/react.md b/docs/pages/frameworks/react.md index 65f37484..570cf45d 100644 --- a/docs/pages/frameworks/react.md +++ b/docs/pages/frameworks/react.md @@ -8,6 +8,10 @@ meta: Shoelace offers a React version of every component to provide an idiomatic experience for React users. You can easily toggle between HTML and React examples throughout the documentation. +:::tip +If you are using React 19+, you may want to try out our [JSX types](/frameworks/jsx/) to use the web components directly in your React components. +::: + ## Installation To add Shoelace to your React app, install the package from npm. diff --git a/package-lock.json b/package-lock.json index e0a215c7..65437bc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@types/react": "^18.3.13", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", + "@wc-toolkit/jsx-types": "^1.2.1", "@web/dev-server-esbuild": "^1.0.3", "@web/test-runner": "^0.19.0", "@web/test-runner-commands": "^0.9.0", @@ -2746,6 +2747,13 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@wc-toolkit/jsx-types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@wc-toolkit/jsx-types/-/jsx-types-1.2.1.tgz", + "integrity": "sha512-Lej3yJz/LEzC9Uuu7gv/WEBhR6GVVYbcs7Uk0eZIULoeIKDHLlZJ5kRcTNFkBhJLdTtag4wfcJcwpHAmc1pBpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@web/browser-logs": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.0.tgz", @@ -17532,6 +17540,12 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "@wc-toolkit/jsx-types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@wc-toolkit/jsx-types/-/jsx-types-1.2.1.tgz", + "integrity": "sha512-Lej3yJz/LEzC9Uuu7gv/WEBhR6GVVYbcs7Uk0eZIULoeIKDHLlZJ5kRcTNFkBhJLdTtag4wfcJcwpHAmc1pBpA==", + "dev": true + }, "@web/browser-logs": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.0.tgz", diff --git a/package.json b/package.json index 47f26e79..4d3339c4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "./dist/shoelace-autoloader.js": "./dist/shoelace-autoloader.js", "./dist/themes": "./dist/themes", "./dist/themes/*": "./dist/themes/*", + "./dist/types/*": "./dist/types/*", "./dist/components": "./dist/components", "./dist/components/*": "./dist/components/*", "./dist/utilities": "./dist/utilities", @@ -87,6 +88,7 @@ "@types/react": "^18.3.13", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", + "@wc-toolkit/jsx-types": "^1.2.2", "@web/dev-server-esbuild": "^1.0.3", "@web/test-runner": "^0.19.0", "@web/test-runner-commands": "^0.9.0", diff --git a/src/components/alert/alert.component.ts b/src/components/alert/alert.component.ts index 7b442cd6..a47170aa 100644 --- a/src/components/alert/alert.component.ts +++ b/src/components/alert/alert.component.ts @@ -60,8 +60,10 @@ export default class SlAlert extends ShoelaceElement { return this.currentToastStack; } + /** @internal */ @query('[part~="base"]') base: HTMLElement; + /** @internal */ @query('.alert__countdown-elapsed') countdownElement: HTMLElement; /** diff --git a/src/components/animated-image/animated-image.component.ts b/src/components/animated-image/animated-image.component.ts index c5a57474..a34b5f6e 100644 --- a/src/components/animated-image/animated-image.component.ts +++ b/src/components/animated-image/animated-image.component.ts @@ -30,9 +30,13 @@ export default class SlAnimatedImage extends ShoelaceElement { static styles: CSSResultGroup = [componentStyles, styles]; static dependencies = { 'sl-icon': SlIcon }; + /** @internal */ @query('.animated-image__animated') animatedImage: HTMLImageElement; + /** @internal */ @state() frozenFrame: string; + + /** @internal */ @state() isLoaded = false; /** The path to the image to load. */ diff --git a/src/components/animation/animation.component.ts b/src/components/animation/animation.component.ts index 3a0d3257..ab004db8 100644 --- a/src/components/animation/animation.component.ts +++ b/src/components/animation/animation.component.ts @@ -26,6 +26,7 @@ export default class SlAnimation extends ShoelaceElement { private animation?: Animation; private hasStarted = false; + /** @internal */ @queryAsync('slot') defaultSlot: Promise; /** The name of the built-in animation to use. For custom animations, use the `keyframes` prop. */ diff --git a/src/components/breadcrumb-item/breadcrumb-item.component.ts b/src/components/breadcrumb-item/breadcrumb-item.component.ts index 2afbac1d..682d41ab 100644 --- a/src/components/breadcrumb-item/breadcrumb-item.component.ts +++ b/src/components/breadcrumb-item/breadcrumb-item.component.ts @@ -32,6 +32,7 @@ export default class SlBreadcrumbItem extends ShoelaceElement { private readonly hasSlotController = new HasSlotController(this, 'prefix', 'suffix'); + /** @internal */ @query('slot:not([name])') defaultSlot: HTMLSlotElement; @state() private renderType: 'button' | 'link' | 'dropdown' = 'button'; diff --git a/src/components/breadcrumb/breadcrumb.component.ts b/src/components/breadcrumb/breadcrumb.component.ts index 4f314935..59521100 100644 --- a/src/components/breadcrumb/breadcrumb.component.ts +++ b/src/components/breadcrumb/breadcrumb.component.ts @@ -28,7 +28,10 @@ export default class SlBreadcrumb extends ShoelaceElement { private readonly localize = new LocalizeController(this); private separatorDir = this.localize.dir(); + /** @internal */ @query('slot') defaultSlot: HTMLSlotElement; + + /** @internal */ @query('slot[name="separator"]') separatorSlot: HTMLSlotElement; /** diff --git a/src/components/button-group/button-group.component.ts b/src/components/button-group/button-group.component.ts index a842bdf9..7b47a103 100644 --- a/src/components/button-group/button-group.component.ts +++ b/src/components/button-group/button-group.component.ts @@ -18,8 +18,10 @@ import type { CSSResultGroup } from 'lit'; export default class SlButtonGroup extends ShoelaceElement { static styles: CSSResultGroup = [componentStyles, styles]; + /** @internal */ @query('slot') defaultSlot: HTMLSlotElement; + /** @internal */ @state() disableRole = false; /** diff --git a/src/components/button/button.component.ts b/src/components/button/button.component.ts index f0bd228f..3f68e848 100644 --- a/src/components/button/button.component.ts +++ b/src/components/button/button.component.ts @@ -52,10 +52,15 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix'); private readonly localize = new LocalizeController(this); + /** @internal */ @query('.button') button: HTMLButtonElement | HTMLLinkElement; @state() private hasFocus = false; + + /** @internal */ @state() invalid = false; + + /** @internal */ @property() title = ''; // make reactive to pass through /** The button's theme variant. */ diff --git a/src/components/carousel/carousel.component.ts b/src/components/carousel/carousel.component.ts index dd0c3e49..401116b0 100644 --- a/src/components/carousel/carousel.component.ts +++ b/src/components/carousel/carousel.component.ts @@ -81,14 +81,19 @@ export default class SlCarousel extends ShoelaceElement { /** When set, it is possible to scroll through the slides by dragging them with the mouse. */ @property({ type: Boolean, reflect: true, attribute: 'mouse-dragging' }) mouseDragging = false; + /** @internal */ @query('.carousel__slides') scrollContainer: HTMLElement; + + /** @internal */ @query('.carousel__pagination') paginationContainer: HTMLElement; - // The index of the active slide + /** @internal The index of the active slide */ @state() activeSlide = 0; + /** @internal */ @state() scrolling = false; + /** @internal */ @state() dragging = false; private autoplayController = new AutoplayController(this, () => this.next()); diff --git a/src/components/checkbox/checkbox.component.ts b/src/components/checkbox/checkbox.component.ts index 603dd4e6..30fadf49 100644 --- a/src/components/checkbox/checkbox.component.ts +++ b/src/components/checkbox/checkbox.component.ts @@ -52,10 +52,12 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC }); private readonly hasSlotController = new HasSlotController(this, 'help-text'); + /** @internal */ @query('input[type="checkbox"]') input: HTMLInputElement; @state() private hasFocus = false; + /** @internal */ @property() title = ''; // make reactive to pass through /** The name of the checkbox, submitted as a name/value pair with form data. */ diff --git a/src/components/color-picker/color-picker.component.ts b/src/components/color-picker/color-picker.component.ts index b9fb6889..a002e5b1 100644 --- a/src/components/color-picker/color-picker.component.ts +++ b/src/components/color-picker/color-picker.component.ts @@ -106,10 +106,19 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo private isSafeValue = false; private readonly localize = new LocalizeController(this); + /** @internal */ @query('[part~="base"]') base: HTMLElement; + + /** @internal */ @query('[part~="input"]') input: SlInput; + + /** @internal */ @query('.color-dropdown') dropdown: SlDropdown; + + /** @internal */ @query('[part~="preview"]') previewButton: HTMLButtonElement; + + /** @internal */ @query('[part~="trigger"]') trigger: HTMLButtonElement; @state() private hasFocus = false; diff --git a/src/components/copy-button/copy-button.component.ts b/src/components/copy-button/copy-button.component.ts index cdcbbd80..b1354f6a 100644 --- a/src/components/copy-button/copy-button.component.ts +++ b/src/components/copy-button/copy-button.component.ts @@ -50,12 +50,22 @@ export default class SlCopyButton extends ShoelaceElement { private readonly localize = new LocalizeController(this); + /** @internal */ @query('slot[name="copy-icon"]') copyIcon: HTMLSlotElement; + + /** @internal */ @query('slot[name="success-icon"]') successIcon: HTMLSlotElement; + + /** @internal */ @query('slot[name="error-icon"]') errorIcon: HTMLSlotElement; + + /** @internal */ @query('sl-tooltip') tooltip: SlTooltip; + /** @internal */ @state() isCopying = false; + + /** @internal */ @state() status: 'rest' | 'success' | 'error' = 'rest'; /** The text value to copy. */ diff --git a/src/components/details/details.component.ts b/src/components/details/details.component.ts index a1c4b55c..62f49791 100644 --- a/src/components/details/details.component.ts +++ b/src/components/details/details.component.ts @@ -48,11 +48,19 @@ export default class SlDetails extends ShoelaceElement { private readonly localize = new LocalizeController(this); + /** @internal */ @query('.details') details: HTMLDetailsElement; + + /** @internal */ @query('.details__header') header: HTMLElement; + + /** @internal */ @query('.details__body') body: HTMLElement; + + /** @internal */ @query('.details__expand-icon-slot') expandIconSlot: HTMLSlotElement; + /** @internal */ detailsObserver: MutationObserver; /** @@ -127,6 +135,7 @@ export default class SlDetails extends ShoelaceElement { } } + /** @internal */ @watch('open', { waitUntilFirstUpdate: true }) async handleOpenChange() { if (this.open) { diff --git a/src/components/dialog/dialog.component.ts b/src/components/dialog/dialog.component.ts index 724b9c0b..9d9d3c8c 100644 --- a/src/components/dialog/dialog.component.ts +++ b/src/components/dialog/dialog.component.ts @@ -76,11 +76,18 @@ export default class SlDialog extends ShoelaceElement { private readonly hasSlotController = new HasSlotController(this, 'footer'); private readonly localize = new LocalizeController(this); private originalTrigger: HTMLElement | null; - public modal = new Modal(this); private closeWatcher: CloseWatcher | null; + + /** @internal */ + public modal = new Modal(this); + /** @internal */ @query('.dialog') dialog: HTMLElement; + + /** @internal */ @query('.dialog__panel') panel: HTMLElement; + + /** @internal */ @query('.dialog__overlay') overlay: HTMLElement; /** @@ -155,6 +162,7 @@ export default class SlDialog extends ShoelaceElement { } }; + /** @internal */ @watch('open', { waitUntilFirstUpdate: true }) async handleOpenChange() { if (this.open) { @@ -333,6 +341,7 @@ setDefaultAnimation('dialog.show', { options: { duration: 250, easing: 'ease' } }); +/** @internal */ setDefaultAnimation('dialog.hide', { keyframes: [ { opacity: 1, scale: 1 }, diff --git a/src/components/drawer/drawer.component.ts b/src/components/drawer/drawer.component.ts index a1ce0c52..28a081ce 100644 --- a/src/components/drawer/drawer.component.ts +++ b/src/components/drawer/drawer.component.ts @@ -82,11 +82,18 @@ export default class SlDrawer extends ShoelaceElement { private readonly hasSlotController = new HasSlotController(this, 'footer'); private readonly localize = new LocalizeController(this); private originalTrigger: HTMLElement | null; - public modal = new Modal(this); private closeWatcher: CloseWatcher | null; + + /** @internal */ + public modal = new Modal(this); + /** @internal */ @query('.drawer') drawer: HTMLElement; + + /** @internal */ @query('.drawer__panel') panel: HTMLElement; + + /** @internal */ @query('.drawer__overlay') overlay: HTMLElement; /** @@ -179,6 +186,7 @@ export default class SlDrawer extends ShoelaceElement { } }; + /** @internal */ @watch('open', { waitUntilFirstUpdate: true }) async handleOpenChange() { if (this.open) { @@ -281,6 +289,7 @@ export default class SlDrawer extends ShoelaceElement { } } + /** @internal */ @watch('contained', { waitUntilFirstUpdate: true }) handleNoModalChange() { if (this.open && !this.contained) { diff --git a/src/components/dropdown/dropdown.component.ts b/src/components/dropdown/dropdown.component.ts index 4dea944c..350ff250 100644 --- a/src/components/dropdown/dropdown.component.ts +++ b/src/components/dropdown/dropdown.component.ts @@ -47,8 +47,13 @@ export default class SlDropdown extends ShoelaceElement { static styles: CSSResultGroup = [componentStyles, styles]; static dependencies = { 'sl-popup': SlPopup }; + /** @internal */ @query('.dropdown') popup: SlPopup; + + /** @internal */ @query('.dropdown__trigger') trigger: HTMLSlotElement; + + /** @internal */ @query('.dropdown__panel') panel: HTMLSlotElement; private readonly localize = new LocalizeController(this); @@ -229,7 +234,7 @@ export default class SlDropdown extends ShoelaceElement { } }; - handleTriggerClick() { + private handleTriggerClick() { if (this.open) { this.hide(); } else { @@ -238,7 +243,7 @@ export default class SlDropdown extends ShoelaceElement { } } - async handleTriggerKeyDown(event: KeyboardEvent) { + private async handleTriggerKeyDown(event: KeyboardEvent) { // When spacebar/enter is pressed, show the panel but don't focus on the menu. This let's the user press the same // key again to hide the menu in case they don't want to make a selection. if ([' ', 'Enter'].includes(event.key)) { @@ -286,14 +291,14 @@ export default class SlDropdown extends ShoelaceElement { } } - handleTriggerKeyUp(event: KeyboardEvent) { + private handleTriggerKeyUp(event: KeyboardEvent) { // Prevent space from triggering a click event in Firefox if (event.key === ' ') { event.preventDefault(); } } - handleTriggerSlotChange() { + private handleTriggerSlotChange() { this.updateAccessibleTrigger(); } @@ -307,7 +312,7 @@ export default class SlDropdown extends ShoelaceElement { // // To determine this, we assume the first tabbable element in the trigger slot is the "accessible trigger." // - updateAccessibleTrigger() { + private updateAccessibleTrigger() { const assignedElements = this.trigger.assignedElements({ flatten: true }) as HTMLElement[]; const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start); let target: HTMLElement; @@ -357,7 +362,7 @@ export default class SlDropdown extends ShoelaceElement { this.popup.reposition(); } - addOpenListeners() { + private addOpenListeners() { this.panel.addEventListener('sl-select', this.handlePanelSelect); if ('CloseWatcher' in window) { this.closeWatcher?.destroy(); @@ -373,7 +378,7 @@ export default class SlDropdown extends ShoelaceElement { document.addEventListener('mousedown', this.handleDocumentMouseDown); } - removeOpenListeners() { + private removeOpenListeners() { if (this.panel) { this.panel.removeEventListener('sl-select', this.handlePanelSelect); this.panel.removeEventListener('keydown', this.handleKeyDown); @@ -383,6 +388,7 @@ export default class SlDropdown extends ShoelaceElement { this.closeWatcher?.destroy(); } + /** @internal */ @watch('open', { waitUntilFirstUpdate: true }) async handleOpenChange() { if (this.disabled) { diff --git a/src/components/icon-button/icon-button.component.ts b/src/components/icon-button/icon-button.component.ts index 587f19a3..05f8d690 100644 --- a/src/components/icon-button/icon-button.component.ts +++ b/src/components/icon-button/icon-button.component.ts @@ -25,6 +25,7 @@ export default class SlIconButton extends ShoelaceElement { static styles: CSSResultGroup = [componentStyles, styles]; static dependencies = { 'sl-icon': SlIcon }; + /** @internal */ @query('.icon-button') button: HTMLButtonElement | HTMLLinkElement; @state() private hasFocus = false; @@ -76,17 +77,17 @@ export default class SlIconButton extends ShoelaceElement { } } - /** Simulates a click on the icon button. */ + /** @internal Simulates a click on the icon button. */ click() { this.button.click(); } - /** Sets focus on the icon button. */ + /** @internal Sets focus on the icon button. */ focus(options?: FocusOptions) { this.button.focus(options); } - /** Removes focus from the icon button. */ + /** @internal Removes focus from the icon button. */ blur() { this.button.blur(); } diff --git a/src/components/image-comparer/image-comparer.component.ts b/src/components/image-comparer/image-comparer.component.ts index 9f9cebf9..3ce6cc36 100644 --- a/src/components/image-comparer/image-comparer.component.ts +++ b/src/components/image-comparer/image-comparer.component.ts @@ -41,7 +41,10 @@ export default class SlImageComparer extends ShoelaceElement { private readonly localize = new LocalizeController(this); + /** @internal */ @query('.image-comparer') base: HTMLElement; + + /** @internal */ @query('.image-comparer__handle') handle: HTMLElement; /** The position of the divider as a percentage. */ @@ -90,6 +93,7 @@ export default class SlImageComparer extends ShoelaceElement { } } + /** @internal */ @watch('position', { waitUntilFirstUpdate: true }) handlePositionChange() { this.emit('sl-change'); diff --git a/src/components/include/include.component.ts b/src/components/include/include.component.ts index d4dbb2ab..052bbd26 100644 --- a/src/components/include/include.component.ts +++ b/src/components/include/include.component.ts @@ -42,6 +42,7 @@ export default class SlInclude extends ShoelaceElement { script.parentNode!.replaceChild(newScript, script); } + /** @internal */ @watch('src') async handleSrcChange() { try { diff --git a/src/components/input/input.component.ts b/src/components/input/input.component.ts index a672a8e0..21d9f527 100644 --- a/src/components/input/input.component.ts +++ b/src/components/input/input.component.ts @@ -60,9 +60,12 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label'); private readonly localize = new LocalizeController(this); + /** @internal */ @query('.input__control') input: HTMLInputElement; @state() private hasFocus = false; + + /** @internal */ @property() title = ''; // make reactive to pass through private __numberInput = Object.assign(document.createElement('input'), { type: 'number' }); @@ -303,12 +306,14 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont this.passwordVisible = !this.passwordVisible; } + /** @internal */ @watch('disabled', { waitUntilFirstUpdate: true }) handleDisabledChange() { // Disabled form controls are always valid this.formControlController.setValidity(this.disabled); } + /** @internal */ @watch('step', { waitUntilFirstUpdate: true }) handleStepChange() { // If step changes, the value may become invalid so we need to recheck after the update. We set the new step @@ -317,18 +322,19 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont this.formControlController.updateValidity(); } + /** @internal */ @watch('value', { waitUntilFirstUpdate: true }) async handleValueChange() { await this.updateComplete; this.formControlController.updateValidity(); } - /** Sets focus on the input. */ + /** @internal Sets focus on the input. */ focus(options?: FocusOptions) { this.input.focus(options); } - /** Removes focus from the input. */ + /** @internal Removes focus from the input. */ blur() { this.input.blur(); } diff --git a/src/components/menu-item/menu-item.component.ts b/src/components/menu-item/menu-item.component.ts index 93d6f758..7d84e684 100644 --- a/src/components/menu-item/menu-item.component.ts +++ b/src/components/menu-item/menu-item.component.ts @@ -50,7 +50,10 @@ export default class SlMenuItem extends ShoelaceElement { private cachedTextLabel: string; private readonly localize = new LocalizeController(this); + /** @internal */ @query('slot:not([name])') defaultSlot: HTMLSlotElement; + + /** @internal */ @query('.menu-item') menuItem: HTMLElement; /** The type of menu item to render. To use `checked`, this value must be set to `checkbox`. */ @@ -112,6 +115,7 @@ export default class SlMenuItem extends ShoelaceElement { event.stopPropagation(); }; + /** @internal */ @watch('checked') handleCheckedChange() { // For proper accessibility, users have to use type="checkbox" to use the checked attribute @@ -129,11 +133,13 @@ export default class SlMenuItem extends ShoelaceElement { } } + /** @internal */ @watch('disabled') handleDisabledChange() { this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } + /** @internal */ @watch('type') handleTypeChange() { if (this.type === 'checkbox') { @@ -150,6 +156,7 @@ export default class SlMenuItem extends ShoelaceElement { return getTextContent(this.defaultSlot); } + /** @internal */ isSubmenu() { return this.hasSlotController.test('submenu'); } diff --git a/src/components/menu/menu.component.ts b/src/components/menu/menu.component.ts index 5b941428..98e03af7 100644 --- a/src/components/menu/menu.component.ts +++ b/src/components/menu/menu.component.ts @@ -23,6 +23,7 @@ export interface MenuSelectEventDetail { export default class SlMenu extends ShoelaceElement { static styles: CSSResultGroup = [componentStyles, styles]; + /** @internal */ @query('slot') defaultSlot: HTMLSlotElement; connectedCallback() { diff --git a/src/components/mutation-observer/mutation-observer.component.ts b/src/components/mutation-observer/mutation-observer.component.ts index f78a9cf9..55770a33 100644 --- a/src/components/mutation-observer/mutation-observer.component.ts +++ b/src/components/mutation-observer/mutation-observer.component.ts @@ -90,6 +90,7 @@ export default class SlMutationObserver extends ShoelaceElement { this.mutationObserver.disconnect(); } + /** @internal */ @watch('disabled') handleDisabledChange() { if (this.disabled) { @@ -99,6 +100,7 @@ export default class SlMutationObserver extends ShoelaceElement { } } + /** @internal */ @watch('attr', { waitUntilFirstUpdate: true }) @watch('attr-old-value', { waitUntilFirstUpdate: true }) @watch('char-data', { waitUntilFirstUpdate: true }) diff --git a/src/components/option/option.component.ts b/src/components/option/option.component.ts index f3950cfe..9afc1c6a 100644 --- a/src/components/option/option.component.ts +++ b/src/components/option/option.component.ts @@ -36,11 +36,17 @@ export default class SlOption extends ShoelaceElement { private isInitialized = false; + /** @internal */ @query('.option__label') defaultSlot: HTMLSlotElement; - @state() current = false; // the user has keyed into the option, but hasn't selected it yet (shows a highlight) - @state() selected = false; // the option is selected and has aria-selected="true" - @state() hasHover = false; // we need this because Safari doesn't honor :hover styles while dragging + /** @internal the user has keyed into the option, but hasn't selected it yet (shows a highlight) */ + @state() current = false; + + /** @internal the option is selected and has aria-selected="true" */ + @state() selected = false; + + /** @internal we need this because Safari doesn't honor :hover styles while dragging */ + @state() hasHover = false; /** * The option's value. When selected, the containing form control will receive this value. The value must be unique @@ -80,16 +86,19 @@ export default class SlOption extends ShoelaceElement { this.hasHover = false; } + /** @internal */ @watch('disabled') handleDisabledChange() { this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } + /** @internal */ @watch('selected') handleSelectedChange() { this.setAttribute('aria-selected', this.selected ? 'true' : 'false'); } + /** @internal */ @watch('value') handleValueChange() { // Ensure the value is a string. This ensures the next line doesn't error and allows framework users to pass numbers diff --git a/src/components/progress-ring/progress-ring.component.ts b/src/components/progress-ring/progress-ring.component.ts index fffd900d..5ade4ffa 100644 --- a/src/components/progress-ring/progress-ring.component.ts +++ b/src/components/progress-ring/progress-ring.component.ts @@ -29,8 +29,10 @@ export default class SlProgressRing extends ShoelaceElement { private readonly localize = new LocalizeController(this); + /** @internal */ @query('.progress-ring__indicator') indicator: SVGCircleElement; + /** @internal */ @state() indicatorOffset: string; /** The current progress as a percentage, 0 to 100. */ diff --git a/src/components/qr-code/qr-code.component.ts b/src/components/qr-code/qr-code.component.ts index f28ab50b..f03d6be0 100644 --- a/src/components/qr-code/qr-code.component.ts +++ b/src/components/qr-code/qr-code.component.ts @@ -19,6 +19,7 @@ import type { CSSResultGroup } from 'lit'; export default class SlQrCode extends ShoelaceElement { static styles: CSSResultGroup = [componentStyles, styles]; + /** @internal */ @query('canvas') canvas: HTMLElement; /** The QR code's value. */ @@ -46,6 +47,7 @@ export default class SlQrCode extends ShoelaceElement { this.generate(); } + /** @internal */ @watch(['background', 'errorCorrection', 'fill', 'radius', 'size', 'value']) generate() { if (!this.hasUpdated) { diff --git a/src/components/radio-button/radio-button.component.ts b/src/components/radio-button/radio-button.component.ts index dbd85a75..e062e14a 100644 --- a/src/components/radio-button/radio-button.component.ts +++ b/src/components/radio-button/radio-button.component.ts @@ -34,7 +34,10 @@ export default class SlRadioButton extends ShoelaceElement { private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix'); + /** @internal */ @query('.button') input: HTMLInputElement; + + /** @internal */ @query('.hidden-input') hiddenInput: HTMLInputElement; @state() protected hasFocus = false; @@ -85,17 +88,18 @@ export default class SlRadioButton extends ShoelaceElement { this.emit('sl-focus'); } + /** @internal */ @watch('disabled', { waitUntilFirstUpdate: true }) handleDisabledChange() { this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } - /** Sets focus on the radio button. */ + /** @internal Sets focus on the radio button. */ focus(options?: FocusOptions) { this.input.focus(options); } - /** Removes focus from the radio button. */ + /** @internal Removes focus from the radio button. */ blur() { this.input.blur(); } diff --git a/src/components/radio-group/radio-group.component.ts b/src/components/radio-group/radio-group.component.ts index c1b4dc4b..e924752f 100644 --- a/src/components/radio-group/radio-group.component.ts +++ b/src/components/radio-group/radio-group.component.ts @@ -52,11 +52,16 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor private customValidityMessage = ''; private validationTimeout: number; + /** @internal */ @query('slot:not([name])') defaultSlot: HTMLSlotElement; + + /** @internal */ @query('.radio-group__validation-input') validationInput: HTMLInputElement; @state() private hasButtonGroup = false; @state() private errorMessage = ''; + + /** @internal */ @state() defaultValue = ''; /** @@ -261,11 +266,13 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor this.formControlController.setValidity(this.validity.valid); } + /** @internal */ @watch('size', { waitUntilFirstUpdate: true }) handleSizeChange() { this.syncRadios(); } + /** @internal */ @watch('value') handleValueChange() { if (this.hasUpdated) { @@ -318,7 +325,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor this.formControlController.updateValidity(); } - /** Sets focus on the radio-group. */ + /** @internal Sets focus on the radio-group. */ public focus(options?: FocusOptions) { const radios = this.getAllRadios(); const checked = radios.find(radio => radio.checked); diff --git a/src/components/radio/radio.component.ts b/src/components/radio/radio.component.ts index 3a3a0ceb..b82ec15e 100644 --- a/src/components/radio/radio.component.ts +++ b/src/components/radio/radio.component.ts @@ -31,6 +31,7 @@ export default class SlRadio extends ShoelaceElement { static styles: CSSResultGroup = [componentStyles, styles]; static dependencies = { 'sl-icon': SlIcon }; + /** @internal */ @state() checked = false; @state() protected hasFocus = false; @@ -80,12 +81,14 @@ export default class SlRadio extends ShoelaceElement { this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } + /** @internal */ @watch('checked') handleCheckedChange() { this.setAttribute('aria-checked', this.checked ? 'true' : 'false'); this.setAttribute('tabindex', this.checked ? '0' : '-1'); } + /** @internal */ @watch('disabled', { waitUntilFirstUpdate: true }) handleDisabledChange() { this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); diff --git a/src/components/range/range.component.ts b/src/components/range/range.component.ts index c63fd4c0..6c12fab5 100644 --- a/src/components/range/range.component.ts +++ b/src/components/range/range.component.ts @@ -53,11 +53,16 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont private readonly localize = new LocalizeController(this); private resizeObserver: ResizeObserver; + /** @internal */ @query('.range__control') input: HTMLInputElement; + + /** @internal */ @query('.range__tooltip') output: HTMLOutputElement | null; @state() private hasFocus = false; @state() private hasTooltip = false; + + /** @internal */ @property() title = ''; // make reactive to pass through /** The name of the range, submitted as a name/value pair with form data. */ @@ -191,6 +196,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont } } + /** @internal */ @watch('value', { waitUntilFirstUpdate: true }) handleValueChange() { this.formControlController.updateValidity(); @@ -203,12 +209,14 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont this.syncRange(); } + /** @internal */ @watch('disabled', { waitUntilFirstUpdate: true }) handleDisabledChange() { // Disabled form controls are always valid this.formControlController.setValidity(this.disabled); } + /** @internal */ @watch('hasTooltip', { waitUntilFirstUpdate: true }) syncRange() { const percent = Math.max(0, (this.value - this.min) / (this.max - this.min)); @@ -226,12 +234,12 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont this.formControlController.emitInvalidEvent(event); } - /** Sets focus on the range. */ + /** @internal Sets focus on the range. */ focus(options?: FocusOptions) { this.input.focus(options); } - /** Removes focus from the range. */ + /** @internal Removes focus from the range. */ blur() { this.input.blur(); } From d689c440d17bbe31c85f34fb0195cba1f434cd52 Mon Sep 17 00:00:00 2001 From: Burton Smith Date: Thu, 5 Jun 2025 07:36:18 -0400 Subject: [PATCH 2/3] update dependency --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65437bc3..5b31a411 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@types/react": "^18.3.13", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", - "@wc-toolkit/jsx-types": "^1.2.1", + "@wc-toolkit/jsx-types": "^1.2.2", "@web/dev-server-esbuild": "^1.0.3", "@web/test-runner": "^0.19.0", "@web/test-runner-commands": "^0.9.0", @@ -2748,9 +2748,9 @@ "dev": true }, "node_modules/@wc-toolkit/jsx-types": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@wc-toolkit/jsx-types/-/jsx-types-1.2.1.tgz", - "integrity": "sha512-Lej3yJz/LEzC9Uuu7gv/WEBhR6GVVYbcs7Uk0eZIULoeIKDHLlZJ5kRcTNFkBhJLdTtag4wfcJcwpHAmc1pBpA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@wc-toolkit/jsx-types/-/jsx-types-1.2.2.tgz", + "integrity": "sha512-o7h7Ku1B8khvKyH8zBWT8B0uaz3STUt9x1STrt/0/bgw8zZe4/Wn3b76Qe1VIbTg094UDdFc4C4xFRgvu2yyUQ==", "dev": true, "license": "MIT" }, @@ -17541,9 +17541,9 @@ "dev": true }, "@wc-toolkit/jsx-types": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@wc-toolkit/jsx-types/-/jsx-types-1.2.1.tgz", - "integrity": "sha512-Lej3yJz/LEzC9Uuu7gv/WEBhR6GVVYbcs7Uk0eZIULoeIKDHLlZJ5kRcTNFkBhJLdTtag4wfcJcwpHAmc1pBpA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@wc-toolkit/jsx-types/-/jsx-types-1.2.2.tgz", + "integrity": "sha512-o7h7Ku1B8khvKyH8zBWT8B0uaz3STUt9x1STrt/0/bgw8zZe4/Wn3b76Qe1VIbTg094UDdFc4C4xFRgvu2yyUQ==", "dev": true }, "@web/browser-logs": { From af059b8168edc4e1b0351138fd6d8da7096280d3 Mon Sep 17 00:00:00 2001 From: Burton Smith Date: Thu, 5 Jun 2025 09:59:33 -0400 Subject: [PATCH 3/3] update formatting --- custom-elements-manifest.config.js | 5 +++-- src/components/dialog/dialog.component.ts | 2 +- src/components/drawer/drawer.component.ts | 2 +- src/components/option/option.component.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/custom-elements-manifest.config.js b/custom-elements-manifest.config.js index bc25485f..2af8eade 100644 --- a/custom-elements-manifest.config.js +++ b/custom-elements-manifest.config.js @@ -2,7 +2,7 @@ import * as path from 'path'; import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration'; import { customElementVsCodePlugin } from 'custom-element-vs-code-integration'; import { customElementVuejsPlugin } from 'custom-element-vuejs-integration'; -import { jsxTypesPlugin } from "@wc-toolkit/jsx-types"; +import { jsxTypesPlugin } from '@wc-toolkit/jsx-types'; import { parse } from 'comment-parser'; import { pascalCase } from 'pascal-case'; import commandLineArgs from 'command-line-args'; @@ -232,7 +232,8 @@ export default { fileName: 'index.d.ts', allowUnknownProps: true, defaultExport: true, - componentTypePath: (_, tag) => `../../components/${tag?.replace('sl-', '')}/${tag?.replace('sl-', '')}.component.js` + componentTypePath: (_, tag) => + `../../components/${tag?.replace('sl-', '')}/${tag?.replace('sl-', '')}.component.js` }) ] }; diff --git a/src/components/dialog/dialog.component.ts b/src/components/dialog/dialog.component.ts index 9d9d3c8c..5ad1dbcb 100644 --- a/src/components/dialog/dialog.component.ts +++ b/src/components/dialog/dialog.component.ts @@ -77,7 +77,7 @@ export default class SlDialog extends ShoelaceElement { private readonly localize = new LocalizeController(this); private originalTrigger: HTMLElement | null; private closeWatcher: CloseWatcher | null; - + /** @internal */ public modal = new Modal(this); diff --git a/src/components/drawer/drawer.component.ts b/src/components/drawer/drawer.component.ts index 28a081ce..9970d93e 100644 --- a/src/components/drawer/drawer.component.ts +++ b/src/components/drawer/drawer.component.ts @@ -83,7 +83,7 @@ export default class SlDrawer extends ShoelaceElement { private readonly localize = new LocalizeController(this); private originalTrigger: HTMLElement | null; private closeWatcher: CloseWatcher | null; - + /** @internal */ public modal = new Modal(this); diff --git a/src/components/option/option.component.ts b/src/components/option/option.component.ts index 9afc1c6a..f34e2450 100644 --- a/src/components/option/option.component.ts +++ b/src/components/option/option.component.ts @@ -41,7 +41,7 @@ export default class SlOption extends ShoelaceElement { /** @internal the user has keyed into the option, but hasn't selected it yet (shows a highlight) */ @state() current = false; - + /** @internal the option is selected and has aria-selected="true" */ @state() selected = false;