kopia lustrzana https://github.com/shoelace-style/shoelace
Merge af059b8168
into 1ef287c3c1
commit
20331db715
|
@ -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`
|
||||
})
|
||||
]
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
/>
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -42,6 +42,7 @@ export default class SlInclude extends ShoelaceElement {
|
|||
script.parentNode!.replaceChild(newScript, script);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@watch('src')
|
||||
async handleSrcChange() {
|
||||
try {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface MenuSelectEventDetail {
|
|||
export default class SlMenu extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
/** @internal */
|
||||
@query('slot') defaultSlot: HTMLSlotElement;
|
||||
|
||||
connectedCallback() {
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue