pull/2455/merge
Burton Smith 2025-08-01 12:58:45 -07:00 zatwierdzone przez GitHub
commit 20331db715
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
35 zmienionych plików z 342 dodań i 23 usunięć

Wyświetl plik

@ -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,14 @@ 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`
})
]
};

Wyświetl plik

@ -18,6 +18,7 @@
<li><a href="/frameworks/vue">Vue</a></li>
<li><a href="/frameworks/angular">Angular</a></li>
<li><a href="/frameworks/svelte">Svelte</a></li>
<li><a href="/frameworks/jsx">JSX</a></li>
</ul>
</li>
<li>

Wyświetl plik

@ -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 (
<sl-card>
<h2 slot="header">Welcome to Shoelace</h2>
<p>This is a Shoelace card component used in JSX.</p>
<sl-button variant="primary" size="medium" onClick={handleClick}>
<sl-icon slot="prefix" name="star"></sl-icon>
Star
</sl-button>
</sl-card>
);
}
```
## 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 (
<div>
<sl-input placeholder="Type something..." onsl-input={handleInput} />
<sl-select placeholder="Choose an option" onsl-change={handleChange}>
<sl-option value="option1">Option 1</sl-option>
<sl-option value="option2">Option 2</sl-option>
<sl-option value="option3">Option 3</sl-option>
</sl-select>
</div>
);
}
```
## 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 (
<sl-dialog label="Dialog Title" open>
<p>This content goes in the default slot.</p>
<sl-button slot="footer" variant="default">
Cancel
</sl-button>
<sl-button slot="footer" variant="primary">
Confirm
</sl-button>
</sl-dialog>
);
}
```
## Type Safety
The JSX types provide full IntelliSense support and type checking:
```tsx
// ✅ Valid - all properties are properly typed
<sl-button
variant="primary" // Type: "default" | "primary" | "success" | "neutral" | "warning" | "danger" | "text"
size="large" // Type: "small" | "medium" | "large"
disabled={false} // Type: boolean
loading={true} // Type: boolean
/>
// ❌ Invalid - TypeScript will catch these errors
<sl-button
variant="invalid" // Error: Type '"invalid"' is not assignable
size="huge" // Error: Type '"huge"' is not assignable
disabled="false" // Error: Type 'string' is not assignable to type 'boolean'
/>
```

Wyświetl plik

@ -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.

14
package-lock.json wygenerowano
Wyświetl plik

@ -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.2",
"@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.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"
},
"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.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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.0.tgz",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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;
/**

Wyświetl plik

@ -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. */

Wyświetl plik

@ -26,6 +26,7 @@ export default class SlAnimation extends ShoelaceElement {
private animation?: Animation;
private hasStarted = false;
/** @internal */
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
/** The name of the built-in animation to use. For custom animations, use the `keyframes` prop. */

Wyświetl plik

@ -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';

Wyświetl plik

@ -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;
/**

Wyświetl plik

@ -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;
/**

Wyświetl plik

@ -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. */

Wyświetl plik

@ -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());

Wyświetl plik

@ -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. */

Wyświetl plik

@ -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;

Wyświetl plik

@ -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. */

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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 },

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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');

Wyświetl plik

@ -42,6 +42,7 @@ export default class SlInclude extends ShoelaceElement {
script.parentNode!.replaceChild(newScript, script);
}
/** @internal */
@watch('src')
async handleSrcChange() {
try {

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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');
}

Wyświetl plik

@ -23,6 +23,7 @@ export interface MenuSelectEventDetail {
export default class SlMenu extends ShoelaceElement {
static styles: CSSResultGroup = [componentStyles, styles];
/** @internal */
@query('slot') defaultSlot: HTMLSlotElement;
connectedCallback() {

Wyświetl plik

@ -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 })

Wyświetl plik

@ -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

Wyświetl plik

@ -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. */

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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');

Wyświetl plik

@ -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();
}