migrate to LitElement

pull/362/head^2
Cory LaViska 2021-03-06 12:01:39 -05:00
rodzic 099dfc93d8
commit 2f4d93700a
60 zmienionych plików z 1656 dodań i 1610 usunięć

Wyświetl plik

@ -31,7 +31,7 @@ If that's not what you're trying to do, the [documentation website](https://shoe
### What are you using to build Shoelace?
Components are built with [Shoemaker](https://github.com/shoelace-style/shoemaker), a lightweight utility that provides an elegant API and reactive data binding. The build is a custom script with bundling powered by [esbuild](https://esbuild.github.io/).
Components are built with [LitElement](https://lit-element.polymer-project.org/), a custom elements base class that provides an intuitive API and reactive data binding. The build is a custom script with bundling powered by [esbuild](https://esbuild.github.io/).
### Forking the Repo

Wyświetl plik

@ -56,6 +56,7 @@
<tr>
<th>Event</th>
<th>Description</th>
<th>Details</th>
</tr>
</thead>
<tbody>
@ -65,6 +66,7 @@
<tr>
<td><code>${escapeHtml(event.name)}</code></td>
<td>${escapeHtml(event.description)}</td>
<td><code style="white-space: normal;">${escapeHtml(event.details)}</code></td>
</tr>
`
)
@ -379,7 +381,7 @@
result += `
## Dependencies
This component has the following dependencies so, if you're [cherry picking](/getting-started/installation#cherry-picking),
This component has the following dependencies so, if you're [cherry picking](/getting-started/installation#cherry-picking),
be sure to import these components in addition to <code>&lt;${tag}&gt;</code>.
${createDependenciesList(component.tag, metadata.components)}

Wyświetl plik

@ -8,8 +8,6 @@ For a full list of icons that come bundled with Shoelace, refer to the [icon com
```html preview
<sl-icon-button name="gear" label="Settings"></sl-icon-button>
<sl-icon-button name="sliders" label="Options"></sl-icon-button>
<sl-icon-button name="x" label="Close"></sl-icon-button>
```
## Examples

Wyświetl plik

@ -53,7 +53,12 @@ Use the `disable` attribute to disable the rating.
### Custom Icons
```html preview
<sl-rating symbol="heart-fill" style="--symbol-color-active: #ff4136;"></sl-rating>
<sl-rating class="rating-hearts" style="--symbol-color-active: #ff4136;"></sl-rating>
<script>
const rating = document.querySelector('.rating-hearts');
rating.getSymbol = () => '<sl-icon name="heart-fill"></sl-icon>';
</script>
```
### Value-based Icons

Wyświetl plik

@ -6,16 +6,19 @@ Components with the <sl-badge type="warning" pill>Experimental</sl-badge> badge
_During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛
## Next
## 2.0.0-beta.29
- Added `vscode.html-custom-data.json` to the build to support IntelliSense
**This release migrates component implementations from Shoemaker to LitElement.** Due to feedback from the community, Shoelace will rely on a more heavily tested library for component implementations. This gives you a more solid foundation and reduces my maintenance burden. Thank you for all your comments, concerns, and encouragement! Aside from that, everything else from beta.28 still applies plus the following.
- 🚨 BREAKING: removed the `symbol` prop from `sl-rating` and reverted to `getSymbol` for optimal flexibility
- Added `vscode.html-custom-data.json` to the build to support IntelliSense (see [the usage section](/getting-started/usage#code-completion) for details)
- Added a style to prevent FOUC before components are defined
- Moved chunk files into a separate folder
- Updated esbuild to 0.8.54
## 2.0.0-beta.28
**This release includes a major under the hood overhaul of the library and how it's distributed.** Until now, Shoelace was developed with Stencil. This release moves to a lightweight tool called [Shoemaker](https://github.com/shoelace-style/shoemaker), a homegrown utility that provides declarative templating and data binding while reducing the boilerplate required for said features. The base class is open source and less than [200 lines of code](https://github.com/shoelace-style/shoemaker/blob/master/src/shoemaker.ts).
**This release includes a major under the hood overhaul of the library and how it's distributed.** Until now, Shoelace was developed with Stencil. This release moves to a lightweight tool called Shoemaker, a homegrown utility that provides declarative templating and data binding while reducing the boilerplate required for said features.
This change in tooling addresses a number of longstanding bugs and limitations. It also gives us more control over the library and build process while streamlining development and maintenance. Instead of two different distributions, Shoelace now offers a single, standards-compliant collection of ES modules. This may affect how you install and use the library, so please refer to the [installation page](/getting-started/installation) for details.

Wyświetl plik

@ -113,7 +113,7 @@ Designing, developing, and supporting this library requires a lot of time, effor
Special thanks to the following projects and individuals that helped make Shoelace possible.
- Components are built with [Shoemaker](https://github.com/shoelace-style/shoemaker)
- Components are built with [LitElement](https://lit-element.polymer-project.org/)
- Documentation is powered by [Docsify](https://docsify.js.org/)
- CDN services are provided by [jsDelivr](https://www.jsdelivr.com/)
- The default theme is based on color palettes from [Tailwind](https://tailwindcss.com/)

18
package-lock.json wygenerowano
Wyświetl plik

@ -118,11 +118,6 @@
"resolved": "https://registry.npmjs.org/@shoelace-style/animations/-/animations-1.1.0.tgz",
"integrity": "sha512-Be+cahtZyI2dPKRm8EZSx3YJQ+jLvEcn3xzRP7tM4tqBnvd/eW/64Xh0iOf0t2w5P8iJKfdBbpVNE9naCaOf2g=="
},
"@shoelace-style/shoemaker": {
"version": "1.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@shoelace-style/shoemaker/-/shoemaker-1.0.0-beta.6.tgz",
"integrity": "sha512-uIogfUXZiczjZj0J5r6yEf0PFcop7aT72xqDK+tdhE68yAQ66Xx/UMNwZJnICozUAoqX0FdNd1gFH2EBJXukOw=="
},
"@sideway/address": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.1.tgz",
@ -2308,6 +2303,19 @@
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
"dev": true
},
"lit-element": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.4.0.tgz",
"integrity": "sha512-pBGLglxyhq/Prk2H91nA0KByq/hx/wssJBQFiYqXhGDvEnY31PRGYf1RglVzyLeRysu0IHm2K0P196uLLWmwFg==",
"requires": {
"lit-html": "^1.1.1"
}
},
"lit-html": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.3.0.tgz",
"integrity": "sha512-0Q1bwmaFH9O14vycPHw8C/IeHMk/uSDldVLIefu/kfbTBGIc44KGH6A8p1bDfxUfHdc8q6Ct7kQklWoHgr4t1Q=="
},
"localtunnel": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.1.tgz",

Wyświetl plik

@ -37,8 +37,9 @@
"dependencies": {
"@popperjs/core": "^2.7.0",
"@shoelace-style/animations": "^1.1.0",
"@shoelace-style/shoemaker": "^1.0.0-beta.6",
"color": "^3.1.3"
"color": "^3.1.3",
"lit-element": "^2.4.0",
"lit-html": "^1.3.0"
},
"devDependencies": {
"@types/color": "^3.0.1",

Wyświetl plik

@ -98,14 +98,12 @@ components.map(async component => {
const dependencies = tags.filter(item => item.tag === 'dependency');
const slots = tags.filter(item => item.tag === 'slot');
const parts = tags.filter(item => item.tag === 'part');
const events = tags.filter(item => item.tag === 'emit');
api.since = tags.find(item => item.tag === 'since').text.trim();
api.status = tags.find(item => item.tag === 'status').text.trim();
api.dependencies = dependencies.map(tag => tag.text.trim());
api.slots = slots.map(tag => splitText(tag.text));
api.parts = parts.map(tag => splitText(tag.text));
api.events = events.map(tag => splitText(tag.text));
} else {
console.error(chalk.yellow(`Missing comment block for ${component.name} - skipping metadata`));
}
@ -113,6 +111,7 @@ components.map(async component => {
// Props
const props = component.children
.filter(child => child.kindString === 'Property' && !child.flags.isStatic)
.filter(child => child.type.name !== 'EventEmitter')
.filter(child => child.comment && child.comment.shortText); // only with comments
props.map(prop => {
@ -127,6 +126,24 @@ components.map(async component => {
});
});
// Events
const events = component.children
.filter(child => child.kindString === 'Property' && !child.flags.isStatic)
.filter(child => child.type.name === 'EventEmitter')
.filter(child => child.comment && child.comment.shortText); // only with comments
events.map(event => {
const decorator = event.decorators.filter(dec => dec.name === 'event')[0];
const name = (decorator ? decorator.arguments.eventName : event.name).replace(/['"`]/g, '');
const details = event.type.typeArguments.map(arg => arg.name).join(', ');
api.events.push({
name,
description: event.comment.shortText,
details
});
});
// Methods
const methods = component.children
.filter(child => child.kindString === 'Method' && !child.flags.isStatic)

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./alert.scss';
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
@ -16,37 +18,45 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
* @part icon - The container that wraps the alert icon.
* @part message - The alert message.
* @part close-button - The close button.
*
* @emit sl-show - Emitted when the alert opens. Calling `event.preventDefault()` will prevent it from being opened.
* @emit sl-after-show - Emitted after the alert opens and all transitions are complete.
* @emit sl-hide - Emitted when the alert closes. Calling `event.preventDefault()` will prevent it from being closed.
* @emit sl-after-hide - Emitted after the alert closes and all transitions are complete.
*/
export default class SlAlert extends Shoemaker {
static tag = 'sl-alert';
static props = ['isVisible', 'open', 'closable', 'type', 'duration'];
static reflect = ['open', 'type'];
static styles = styles;
@customElement('sl-alert')
export class SlAlert extends LitElement {
static styles = unsafeCSS(styles);
private autoHideTimeout: any;
private isVisible = false;
@internalProperty() private isVisible = false;
/** Indicates whether or not the alert is open. You can use this in lieu of the show/hide methods. */
open = false;
@property({ type: Boolean, reflect: true }) open = false;
/** Makes the alert closable. */
closable = false;
@property({ type: Boolean, reflect: true }) closable = false;
/** The type of alert. */
type: 'primary' | 'success' | 'info' | 'warning' | 'danger' = 'primary';
@property({ reflect: true }) type: 'primary' | 'success' | 'info' | 'warning' | 'danger' = 'primary';
/**
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`.
*/
duration = Infinity;
@property({ type: Number }) duration = Infinity;
/** Emitted when the alert opens. Calling `event.preventDefault()` will prevent it from being opened. */
@event('sl-show') slShow: EventEmitter<void>;
/** Emitted after the alert opens and all transitions are complete. */
@event('sl-after-show') slAfterShow: EventEmitter<void>;
/** Emitted when the alert closes. Calling `event.preventDefault()` will prevent it from being closed. */
@event('sl-hide') slHide: EventEmitter<void>;
/** Emitted after the alert closes and all transitions are complete. */
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
onConnect() {
// Show on init if open
if (this.open) {
this.show();
@ -60,7 +70,7 @@ export default class SlAlert extends Shoemaker {
return;
}
const slShow = this.emit('sl-show');
const slShow = this.slShow.emit();
if (slShow.defaultPrevented) {
this.open = false;
return;
@ -81,7 +91,7 @@ export default class SlAlert extends Shoemaker {
return;
}
const slHide = this.emit('sl-hide');
const slHide = this.slHide.emit();
if (slHide.defaultPrevented) {
this.open = true;
return;
@ -143,15 +153,15 @@ export default class SlAlert extends Shoemaker {
// Ensure we only emit one event when the target element is no longer visible
if (event.propertyName === 'opacity' && target.classList.contains('alert')) {
this.isVisible = this.open;
this.open ? this.emit('sl-after-show') : this.emit('sl-after-hide');
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
}
}
watchOpen() {
openChanged() {
this.open ? this.show() : this.hide();
}
watchDuration() {
durationChanged() {
this.restartAutoHide();
}
@ -174,21 +184,25 @@ export default class SlAlert extends Shoemaker {
aria-live="assertive"
aria-atomic="true"
aria-hidden=${this.open ? 'false' : 'true'}
onmousemove=${this.handleMouseMove.bind(this)}
ontransitionend=${this.handleTransitionEnd.bind(this)}
@mousemove=${this.handleMouseMove.bind(this)}
@transitionend=${this.handleTransitionEnd.bind(this)}
>
<span part="icon" class="alert__icon">
<slot name="icon" />
<slot name="icon"></slot>
</span>
<span part="message" class="alert__message">
<slot />
<slot></slot>
</span>
${this.closable
? html`
<span class="alert__close">
<sl-icon-button exportparts="base:close-button" name="x" onclick=${this.handleCloseClick.bind(this)} />
<sl-icon-button
exportparts="base:close-button"
name="x"
@click=${this.handleCloseClick.bind(this)}
></sl-icon-button>
</span>
`
: ''}

Wyświetl plik

@ -1,4 +1,5 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, queryAsync, unsafeCSS } from 'lit-element';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./animation.scss';
import { animations } from './animations';
@ -7,95 +8,134 @@ import { animations } from './animations';
* @status stable
*
* @slot - The element to animate. If multiple elements are to be animated, wrap them in a single container.
*
* @emit sl-cancel - Emitted when the animation is canceled.
* @emit sl-finish - Emitted when the animation finishes.
* @emit sl-start - Emitted when the animation starts or restarts.
*/
export default class SlAnimation extends Shoemaker {
static tag = 'sl-animation';
static props = [
'name',
'delay',
'direction',
'duration',
'easing',
'endDelay',
'fill',
'iterations',
'iterationStart',
'keyframes',
'playbackRate',
'pause'
];
static reflect = ['name', 'pause'];
static styles = styles;
@customElement('sl-animation')
export class SlAnimation extends LitElement {
static styles = unsafeCSS(styles);
private animation: Animation;
private defaultSlot: HTMLSlotElement;
private hasStarted = false;
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
/** The name of the built-in animation to use. For custom animations, use the `keyframes` prop. */
name = 'none';
@property({ reflect: true }) name = 'none';
/** The number of milliseconds to delay the start of the animation. */
delay = 0;
@property({ type: Number }) delay = 0;
/** Determines the direction of playback as well as the behavior when reaching the end of an iteration. */
direction: PlaybackDirection = 'normal';
@property() direction: PlaybackDirection = 'normal';
/** The number of milliseconds each iteration of the animation takes to complete. */
duration = 1000;
@property({ type: Number }) duration = 1000;
/**
* The easing function to use for the animation. This can be a Shoelace easing function or a custom easing function
* such as `cubic-bezier(0, 1, .76, 1.14)`.
*/
easing = 'linear';
@property() easing = 'linear';
/** The number of milliseconds to delay after the active period of an animation sequence. */
endDelay = 0;
@property({ type: Number }) endDelay = 0;
/** Sets how the animation applies styles to its target before and after its execution. */
fill: FillMode = 'auto';
@property() fill: FillMode = 'auto';
/** The number of iterations to run before the animation completes. Defaults to `Infinity`, which loops. */
iterations: number = Infinity;
@property({ type: Number }) iterations: number = Infinity;
/** The offset at which to start the animation, usually between 0 (start) and 1 (end). */
iterationStart = 0;
@property({ attribute: 'iteration-start', type: Number }) iterationStart = 0;
/** The keyframes to use for the animation. If this is set, `name` will be ignored. */
keyframes: Keyframe[];
@property() keyframes: Keyframe[];
/**
* Sets the animation's playback rate. The default is `1`, which plays the animation at a normal speed. Setting this
* to `2`, for example, will double the animation's speed. A negative value can be used to reverse the animation. This
* value can be changed without causing the animation to restart.
*/
playbackRate = 1;
@property({ attribute: 'playback-rate', type: Number }) playbackRate = 1;
/** Pauses the animation. The animation will resume when this prop is removed. */
pause = false;
@property({ type: Boolean }) pause = false;
onReady() {
/** Emitted when the animation is canceled. */
@event('sl-cancel') slCancel: EventEmitter<void>;
/** Emitted when the animation finishes. */
@event('sl-finish') slFinish: EventEmitter<void>;
/** Emitted when the animation starts or restarts. */
@event('sl-start') slStart: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
this.createAnimation();
}
onDisconnect() {
disconnectedCallback() {
super.disconnectedCallback();
this.destroyAnimation();
}
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('pause')) {
this.handlePauseChange();
}
if (changedProps.has('playbackRate')) {
this.handlePlaybackRateChange();
}
if (
[
'name',
'delay',
'direction',
'duration',
'easing',
'endDelay',
'fill',
'iterations',
'iterationsStart',
'keyframes'
].find(prop => changedProps.has(prop))
) {
this.createAnimation();
}
}
handleAnimationFinish() {
this.emit('sl-finish');
this.slFinish.emit();
}
handleAnimationCancel() {
this.emit('sl-cancel');
this.slCancel.emit();
}
handlePauseChange() {
if (this.animation) {
this.pause ? this.animation.pause() : this.animation.play();
if (!this.pause && !this.hasStarted) {
this.hasStarted = true;
this.slStart.emit();
}
return true;
} else {
return false;
}
}
handlePlaybackRateChange() {
this.animation.playbackRate = this.playbackRate;
if (this.animation) {
this.animation.playbackRate = this.playbackRate;
}
}
handleSlotChange() {
@ -103,13 +143,14 @@ export default class SlAnimation extends Shoemaker {
this.createAnimation();
}
createAnimation() {
async createAnimation() {
const easing = animations.easings[this.easing] || this.easing;
const keyframes: Keyframe[] = this.keyframes ? this.keyframes : (animations as any)[this.name];
const element = this.defaultSlot.assignedElements({ flatten: true })[0] as HTMLElement;
const slot = await this.defaultSlot;
const element = slot.assignedElements()[0] as HTMLElement;
if (!element) {
return;
return false;
}
this.destroyAnimation();
@ -131,8 +172,10 @@ export default class SlAnimation extends Shoemaker {
this.animation.pause();
} else {
this.hasStarted = true;
this.emit('sl-start');
this.slStart.emit();
}
return true;
}
destroyAnimation() {
@ -144,56 +187,6 @@ export default class SlAnimation extends Shoemaker {
}
}
// Restart the animation when any of these properties change
watchDelay() {
this.createAnimation();
}
watchDirection() {
this.createAnimation();
}
watchEasing() {
this.createAnimation();
}
watchEndDelay() {
this.createAnimation();
}
watchFill() {
this.createAnimation();
}
watchIterations() {
this.createAnimation();
}
watchIterationStart() {
this.createAnimation();
}
watchKeyframes() {
this.createAnimation();
}
watchName() {
this.createAnimation();
}
watchPause() {
this.pause ? this.animation.pause() : this.animation.play();
if (!this.pause && !this.hasStarted) {
this.hasStarted = true;
this.emit('sl-start');
}
}
watchPlaybackRate() {
this.animation.playbackRate = this.playbackRate;
}
/** Clears all KeyframeEffects caused by this animation and aborts its playback. */
cancel() {
try {
@ -219,8 +212,6 @@ export default class SlAnimation extends Shoemaker {
}
render() {
return html`
<slot ref=${(el: HTMLSlotElement) => (this.defaultSlot = el)} onslotchange=${this.handleSlotChange.bind(this)} />
`;
return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
}
}

Wyświetl plik

@ -1,4 +1,5 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import styles from 'sass:./avatar.scss';
/**
@ -14,25 +15,23 @@ import styles from 'sass:./avatar.scss';
* @part initials - The container that wraps the avatar initials.
* @part image - The avatar image.
*/
export default class SlAvatar extends Shoemaker {
static tag = 'sl-avatar';
static props = ['hasError', 'image', 'alt', 'initials', 'shape'];
static reflect = ['shape'];
static styles = styles;
@customElement('sl-avatar')
export class SlAvatar extends LitElement {
static styles = unsafeCSS(styles);
private hasError = false;
@internalProperty() private hasError = false;
/** The image source to use for the avatar. */
image = '';
@property({ reflect: true }) image: string;
/** Alternative text for the image. */
alt = '';
@property({ reflect: true }) alt: string;
/** Initials to use as a fallback when no image is available (1-2 characters max recommended). */
initials = '';
@property({ reflect: true }) initials: string;
/** The shape of the avatar. */
shape: 'circle' | 'square' | 'rounded' = 'circle';
@property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle';
render() {
return html`
@ -58,7 +57,7 @@ export default class SlAvatar extends Shoemaker {
`}
${this.image && !this.hasError
? html`
<img part="image" class="avatar__image" src="${this.image}" onerror="${() => (this.hasError = true)}" />
<img part="image" class="avatar__image" src="${this.image}" @error="${() => (this.hasError = true)}" />
`
: ''}
</div>

Wyświetl plik

@ -1,4 +1,5 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import styles from 'sass:./badge.scss';
/**
@ -9,20 +10,18 @@ import styles from 'sass:./badge.scss';
*
* @part base - The base wrapper
*/
export default class SlBadge extends Shoemaker {
static tag = 'sl-badge';
static props = ['type', 'pill', 'pulse'];
static reflect = ['type', 'pill', 'pulse'];
static styles = styles;
@customElement('sl-badge')
export class SlBadge extends LitElement {
static styles = unsafeCSS(styles);
/** The badge's type. */
type: 'primary' | 'success' | 'info' | 'warning' | 'danger' = 'primary';
@property({ reflect: true }) type: 'primary' | 'success' | 'info' | 'warning' | 'danger' = 'primary';
/** Draws a pill-style badge with rounded edges. */
pill = false;
@property({ type: Boolean, reflect: true }) pill = false;
/** Makes the badge pulsate to draw attention. */
pulse = false;
@property({ type: Boolean, reflect: true }) pulse = false;
render() {
return html`
@ -40,7 +39,7 @@ export default class SlBadge extends Shoemaker {
})}
role="status"
>
<slot />
<slot></slot>
</span>
`;
}

Wyświetl plik

@ -10,10 +10,6 @@
position: relative;
}
::slotted(.sl-hover) {
::slotted(.sl-focus) {
z-index: 1;
}
::slotted(.sl-focus) {
z-index: 2;
}

Wyświetl plik

@ -1,4 +1,4 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, unsafeCSS } from 'lit-element';
import styles from 'sass:./button-group.scss';
/**
@ -9,29 +9,12 @@ import styles from 'sass:./button-group.scss';
*
* @part base - The component's base wrapper.
*/
export default class SlButtonGroup extends Shoemaker {
static tag = 'sl-button-group';
static props = ['label'];
static styles = styles;
private buttonGroup: HTMLElement;
@customElement('sl-button-group')
export class SlButtonGroup extends LitElement {
static styles = unsafeCSS(styles);
/** A label to use for the button group's `aria-label` attribute. */
label = '';
onReady() {
this.handleFocus = this.handleFocus.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.buttonGroup.addEventListener('sl-focus', this.handleFocus);
this.buttonGroup.addEventListener('sl-blur', this.handleBlur);
}
onDisconnect() {
this.buttonGroup.removeEventListener('sl-focus', this.handleFocus);
this.buttonGroup.removeEventListener('sl-blur', this.handleBlur);
}
@property() label: string;
handleFocus(event: CustomEvent) {
const button = event.target as HTMLElement;
@ -46,12 +29,13 @@ export default class SlButtonGroup extends Shoemaker {
render() {
return html`
<div
ref=${(el: HTMLElement) => (this.buttonGroup = el)}
part="base"
class="button-group"
aria-label=${this.label}
@focusout=${this.handleBlur}
@focusin=${this.handleFocus}
>
<slot />
<slot></slot>
</div>
`;
}

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./button.scss';
import { hasSlot } from '../../internal/slot';
@ -18,80 +20,66 @@ import { hasSlot } from '../../internal/slot';
* @part label - The button's label.
* @part suffix - The suffix container.
* @part caret - The button's caret.
*
* @emit sl-blur - Emitted when the button loses focus.
* @emit sl-focus - Emitted when the button gains focus.
*/
export default class SlButton extends Shoemaker {
static tag = 'sl-button';
static props = [
'hasFocus',
'hasLabel',
'hasPrefix',
'hasSuffix',
'type',
'size',
'caret',
'disabled',
'loading',
'pill',
'circle',
'submit',
'name',
'value',
'href',
'target',
'download'
];
static reflect = ['type', 'size', 'caret', 'disabled', 'loading', 'pill', 'circle', 'submit'];
static styles = styles;
@customElement('sl-button')
export class SlButton extends LitElement {
static styles = unsafeCSS(styles);
private button: HTMLButtonElement | HTMLLinkElement;
private hasFocus = false;
private hasLabel = false;
private hasPrefix = false;
private hasSuffix = false;
button: HTMLButtonElement | HTMLLinkElement;
@internalProperty() private hasFocus = false;
@internalProperty() private hasLabel = false;
@internalProperty() private hasPrefix = false;
@internalProperty() private hasSuffix = false;
/** The button's type. */
type: 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'text' = 'default';
@property({ reflect: true }) type: 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'text' =
'default';
/** The button's size. */
size: 'small' | 'medium' | 'large' = 'medium';
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
/** Draws the button with a caret for use with dropdowns, popovers, etc. */
caret = false;
@property({ type: Boolean, reflect: true }) caret = false;
/** Disables the button. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** Draws the button in a loading state. */
loading = false;
@property({ type: Boolean, reflect: true }) loading = false;
/** Draws a pill-style button with rounded edges. */
pill = false;
@property({ type: Boolean, reflect: true }) pill = false;
/** Draws a circle button. */
circle = false;
@property({ type: Boolean, reflect: true }) circle = false;
/** Indicates if activating the button should submit the form. Ignored when `href` is set. */
submit = false;
@property({ type: Boolean, reflect: true }) submit = false;
/** An optional name for the button. Ignored when `href` is set. */
name: string;
@property() name: string;
/** An optional value for the button. Ignored when `href` is set. */
value: string;
@property() value: string;
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
href: string;
@property() href: string;
/** Tells the browser where to open the link. Only used when `href` is set. */
target: '_blank' | '_parent' | '_self' | '_top';
@property() target: '_blank' | '_parent' | '_self' | '_top';
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
download: string;
@property() download: string;
onConnect() {
/** Emitted when the button loses focus. */
@event('sl-blur') slBlur: EventEmitter<void>;
/** Emitted when the button gains focus. */
@event('sl-focus') slFocus: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
this.handleSlotChange();
}
@ -113,12 +101,12 @@ export default class SlButton extends Shoemaker {
handleBlur() {
this.hasFocus = false;
this.emit('sl-blur');
this.slBlur.emit();
}
handleFocus() {
this.hasFocus = true;
this.emit('sl-focus');
this.slFocus.emit();
}
handleClick(event: MouseEvent) {
@ -133,13 +121,13 @@ export default class SlButton extends Shoemaker {
const interior = html`
<span part="prefix" class="button__prefix">
<slot onslotchange=${this.handleSlotChange.bind(this)} name="prefix" />
<slot @slotchange=${this.handleSlotChange} name="prefix"></slot>
</span>
<span part="label" class="button__label">
<slot onslotchange=${this.handleSlotChange.bind(this)} />
<slot @slotchange=${this.handleSlotChange}></slot>
</span>
<span part="suffix" class="button__suffix">
<slot onslotchange=${this.handleSlotChange.bind(this)} name="suffix" />
<slot @slotchange=${this.handleSlotChange} name="suffix"></slot>
</span>
${this.caret
? html`
@ -157,7 +145,7 @@ export default class SlButton extends Shoemaker {
</span>
`
: ''}
${this.loading ? html`<sl-spinner />` : ''}
${this.loading ? html`<sl-spinner></sl-spinner>` : ''}
`;
const button = html`
@ -186,13 +174,13 @@ export default class SlButton extends Shoemaker {
'button--has-prefix': this.hasPrefix,
'button--has-suffix': this.hasSuffix
})}
disabled=${this.disabled ? true : null}
?disabled=${this.disabled}
type=${this.submit ? 'submit' : 'button'}
name=${this.name}
value=${this.value}
onblur=${this.handleBlur.bind(this)}
onfocus=${this.handleFocus.bind(this)}
onclick=${this.handleClick.bind(this)}
.value=${this.value}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@click=${this.handleClick}
>
${interior}
</button>
@ -228,9 +216,9 @@ export default class SlButton extends Shoemaker {
target=${this.target ? this.target : null}
download=${this.download ? this.download : null}
rel=${this.target ? 'noreferrer noopener' : null}
onblur=${this.handleBlur.bind(this)}
onfocus=${this.handleFocus.bind(this)}
onclick=${this.handleClick.bind(this)}
onblur=${this.handleBlur}
onfocus=${this.handleFocus}
onclick=${this.handleClick}
>
${interior}
</a>

Wyświetl plik

@ -1,4 +1,5 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import styles from 'sass:./card.scss';
import { hasSlot } from '../../internal/slot';
@ -17,16 +18,16 @@ import { hasSlot } from '../../internal/slot';
* @part body - The card's body.
* @part footer - The card's footer, if present.
*/
export default class SlCard extends Shoemaker {
static tag = 'sl-card';
static props = ['hasFooter', 'hasImage', 'hasHeader'];
static styles = styles;
@customElement('sl-card')
export class SlCard extends LitElement {
static styles = unsafeCSS(styles);
private hasFooter = false;
private hasImage = false;
private hasHeader = false;
@internalProperty() private hasFooter = false;
@internalProperty() private hasImage = false;
@internalProperty() private hasHeader = false;
onConnect() {
connectedCallback() {
super.connectedCallback();
this.handleSlotChange();
}
@ -48,19 +49,19 @@ export default class SlCard extends Shoemaker {
})}
>
<div part="image" class="card__image">
<slot name="image" onslotchange=${this.handleSlotChange.bind(this)} />
<slot name="image" onslotchange=${this.handleSlotChange}></slot>
</div>
<div part="header" class="card__header">
<slot name="header" onslotchange=${this.handleSlotChange.bind(this)} />
<slot name="header" onslotchange=${this.handleSlotChange}></slot>
</div>
<div part="body" class="card__body">
<slot />
<slot></slot>
</div>
<div part="footer" class="card__footer">
<slot name="footer" onslotchange=${this.handleSlotChange.bind(this)} />
<slot name="footer" onslotchange=${this.handleSlotChange}></slot>
</div>
</div>
`;

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./checkbox.scss';
let id = 0;
@ -14,44 +16,49 @@ let id = 0;
* @part checked-icon - The container the wraps the checked icon.
* @part indeterminate-icon - The container that wraps the indeterminate icon.
* @part label - The checkbox label.
*
* @emit sl-blur - Emitted when the control loses focus.
* @emit sl-change - Emitted when the control's checked state changes.
* @emit sl-focus - Emitted when the control gains focus.
*/
export default class SlCheckbox extends Shoemaker {
static tag = 'sl-checkbox';
static props = ['hasFocus', 'name', 'value', 'disabled', 'required', 'checked', 'indeterminate', 'invalid'];
static reflect = ['checked', 'indeterminate', 'invalid'];
static styles = styles;
@customElement('sl-checkbox')
export class SlCheckbox extends LitElement {
static styles = unsafeCSS(styles);
@query('input[type="checkbox"]') input: HTMLInputElement;
private inputId = `checkbox-${++id}`;
private labelId = `checkbox-label-${id}`;
private hasFocus = false;
private input: HTMLInputElement;
@internalProperty() private hasFocus = false;
/** The checkbox's name attribute. */
name: string;
@property({ reflect: true }) name: string;
/** The checkbox's value attribute. */
value: string;
@property() value: string;
/** Disables the checkbox. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** Makes the checkbox a required field. */
required = false;
@property({ type: Boolean, reflect: true }) required = false;
/** Draws the checkbox in a checked state. */
checked = false;
@property({ type: Boolean, reflect: true }) checked = false;
/** Draws the checkbox in an indeterminate state. */
indeterminate = false;
@property({ type: Boolean, reflect: true }) indeterminate = false;
/** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */
invalid = false;
@property({ type: Boolean, reflect: true }) invalid = false;
onReady() {
/** Emitted when the control loses focus. */
@event('sl-blur') slBlur: EventEmitter<void>;
/** Emitted when the control's checked state changes. */
@event('sl-change') slChange: EventEmitter<void>;
/** Emitted when the control gains focus. */
@event('sl-focus') slFocus: EventEmitter<void>;
firstUpdated() {
this.input.indeterminate = this.indeterminate;
}
@ -77,18 +84,18 @@ export default class SlCheckbox extends Shoemaker {
}
handleClick() {
this.checked = this.input.checked;
this.checked = !this.checked;
this.indeterminate = false;
}
handleBlur() {
this.hasFocus = false;
this.emit('sl-blur');
this.slBlur.emit();
}
handleFocus() {
this.hasFocus = true;
this.emit('sl-focus');
this.slFocus.emit();
}
handleLabelMouseDown(event: MouseEvent) {
@ -98,16 +105,18 @@ export default class SlCheckbox extends Shoemaker {
}
handleStateChange() {
this.input.checked = this.checked;
this.input.indeterminate = this.indeterminate;
this.emit('sl-change');
if (this.input) {
this.input.checked = this.checked;
this.input.indeterminate = this.indeterminate;
this.slChange.emit();
}
}
watchChecked() {
checkedChanged() {
this.handleStateChange();
}
watchIndeterminate() {
indeterminateChanged() {
this.handleStateChange();
}
@ -123,7 +132,7 @@ export default class SlCheckbox extends Shoemaker {
'checkbox--indeterminate': this.indeterminate
})}
for=${this.inputId}
onmousedown=${this.handleLabelMouseDown.bind(this)}
@mousedown=${this.handleLabelMouseDown}
>
<span part="control" class="checkbox__control">
${this.checked
@ -159,25 +168,24 @@ export default class SlCheckbox extends Shoemaker {
: ''}
<input
ref=${(el: HTMLInputElement) => (this.input = el)}
id=${this.inputId}
type="checkbox"
name=${this.name}
.value=${this.value}
checked=${this.checked ? true : null}
disabled=${this.disabled ? true : null}
required=${this.required ? true : null}
value=${this.value}
?checked=${this.checked}
?disabled=${this.disabled}
?required=${this.required}
role="checkbox"
aria-checked=${this.checked ? 'true' : 'false'}
aria-labelledby=${this.labelId}
onclick=${this.handleClick.bind(this)}
onblur=${this.handleBlur.bind(this)}
onfocus=${this.handleFocus.bind(this)}
@click=${this.handleClick}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
/>
</span>
<span part="label" id=${this.labelId} class="checkbox__label">
<slot />
<slot></slot>
</span>
</label>
`;

Wyświetl plik

@ -1,4 +1,7 @@
import { classMap, html, styleMap, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { styleMap } from 'lit-html/directives/style-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./color-picker.scss';
import { SlDropdown, SlInput } from '../../shoelace';
import color from 'color';
@ -26,98 +29,73 @@ import { clamp } from '../../internal/math';
* @part preview - The preview color.
* @part input - The text input.
* @part format-button - The toggle format button's base.
*
* @emit sl-change - Emitted when the color picker's value changes.
* @emit sl-show - Emitted when the color picker opens. Calling `event.preventDefault()` will prevent it from being opened.
* @emit sl-after - Emitted after the color picker opens and all transitions are complete.
* @emit sl-hide - Emitted when the color picker closes. Calling `event.preventDefault()` will prevent it from being closed.
* @emit sl-after - Emitted after the color picker closes and all transitions are complete.
*/
export default class SlColorPicker extends Shoemaker {
static tag = 'sl-color-picker';
static props = [
'inputValue',
'hue',
'saturation',
'lightness',
'alpha',
'showCopyFeedback',
'value',
'format',
'inline',
'size',
'noFormatToggle',
'name',
'disabled',
'invalid',
'hoist',
'opacity',
'uppercase',
'swatches'
];
static reflect = ['disabled', 'invalid'];
static styles = styles;
@customElement('sl-color-picker')
export class SlColorPicker extends LitElement {
static styles = unsafeCSS(styles);
@query('[part="input"]') input: SlInput;
@query('[part="preview"]') previewButton: HTMLButtonElement;
@query('.color-dropdown') dropdown: SlDropdown;
private alpha = 100;
private bypassValueParse = false;
private dropdown: SlDropdown;
private hue = 0;
private input: SlInput;
private inputValue = '';
private lastValueEmitted: string;
private lightness = 100;
private previewButton: HTMLButtonElement;
private saturation = 100;
private showCopyFeedback = false;
@internalProperty() private inputValue = '';
@internalProperty() private hue = 0;
@internalProperty() private saturation = 100;
@internalProperty() private lightness = 100;
@internalProperty() private alpha = 100;
@internalProperty() private showCopyFeedback = false;
/** The current color. */
value = '#ffffff';
@property() value = '#ffffff';
/**
* The format to use for the display value. If opacity is enabled, these will translate to HEXA, RGBA, and HSLA
* respectively. The color picker will always accept user input in any format (including CSS color names) and convert
* it to the desired format.
*/
format: 'hex' | 'rgb' | 'hsl' = 'hex';
@property() format: 'hex' | 'rgb' | 'hsl' = 'hex';
/** Renders the color picker inline rather than inside a dropdown. */
inline = false;
@property({ type: Boolean }) inline = false;
/** Determines the size of the color picker's trigger. This has no effect on inline color pickers. */
size: 'small' | 'medium' | 'large' = 'medium';
@property() size: 'small' | 'medium' | 'large' = 'medium';
/** Removes the format toggle. */
noFormatToggle = false;
@property({ attribute: 'no-format-toggle', type: Boolean }) noFormatToggle = false;
/** The input's name attribute. */
name = '';
@property() name = '';
/** Disables the color picker. */
disabled = false;
@property({ type: Boolean }) disabled = false;
/**
* This will be true when the control is in an invalid state. Validity is determined by the `setCustomValidity()`
* method using the browser's constraint validation API.
*/
invalid = false;
@property({ type: Boolean }) invalid = false;
/**
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
* `overflow: auto|scroll`.
*/
hoist = false;
@property({ type: Boolean }) hoist = false;
/** Whether to show the opacity slider. */
opacity = false;
@property({ type: Boolean }) opacity = false;
/** By default, the value will be set in lowercase. Set this to true to set it in uppercase instead. */
uppercase = false;
@property({ type: Boolean }) uppercase = false;
/**
* An array of predefined color swatches to display. Can include any format the color picker can parse, including
* HEX(A), RGB(A), HSL(A), and CSS color names.
*/
swatches = [
@property() swatches = [
'#d0021b',
'#f5a623',
'#f8e71c',
@ -136,7 +114,24 @@ export default class SlColorPicker extends Shoemaker {
'#fff'
];
onReady() {
/** Emitted when the color picker's value changes. */
@event('sl-change') slChange: EventEmitter<void>;
/** Emitted when the color picker opens. Calling `event.preventDefault()` will prevent it from being opened. */
@event('sl-show') slShow: EventEmitter<void>;
/** Emitted after the color picker opens and all transitions are complete. */
@event('sl-after-show') slAfterShow: EventEmitter<void>;
/** Emitted when the color picker closes. Calling `event.preventDefault()` will prevent it from being closed. */
@event('sl-hide') slHide: EventEmitter<void>;
/** Emitted after the color picker closes and all transitions are complete. */
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
if (!this.setColor(this.value)) {
this.setColor(`#ffff`);
}
@ -391,22 +386,22 @@ export default class SlColorPicker extends Shoemaker {
handleDropdownShow(event: CustomEvent) {
event.stopPropagation();
this.emit('sl-show');
this.slShow.emit();
}
handleDropdownAfterShow(event: CustomEvent) {
event.stopPropagation();
this.emit('sl-after-show');
this.slAfterShow.emit();
}
handleDropdownHide(event: CustomEvent) {
event.stopPropagation();
this.emit('sl-hide');
this.slHide.emit();
}
handleDropdownAfterHide(event: CustomEvent) {
event.stopPropagation();
this.emit('sl-after-hide');
this.slAfterHide.emit();
this.showCopyFeedback = false;
}
@ -584,15 +579,15 @@ export default class SlColorPicker extends Shoemaker {
this.bypassValueParse = false;
}
watchFormat() {
formatChanged() {
this.syncValues();
}
watchOpacity() {
opacityChanged() {
this.alpha = 100;
}
watchValue(newValue: string, oldValue: string) {
valueChanged(newValue: string, oldValue: string) {
if (!this.bypassValueParse) {
const newColor = this.parseColor(newValue);
@ -608,7 +603,7 @@ export default class SlColorPicker extends Shoemaker {
}
if (this.value !== this.lastValueEmitted) {
this.emit('sl-change');
this.slChange.emit();
this.lastValueEmitted = this.value;
}
}
@ -631,8 +626,8 @@ export default class SlColorPicker extends Shoemaker {
part="grid"
class="color-picker__grid"
style=${styleMap({ backgroundColor: `hsl(${this.hue}deg, 100%, 50%)` })}
onmousedown=${this.handleGridDrag.bind(this)}
ontouchstart=${this.handleGridDrag.bind(this)}
@mousedown=${this.handleGridDrag}
@touchstart=${this.handleGridDrag}
>
<span
part="grid-handle"
@ -648,8 +643,8 @@ export default class SlColorPicker extends Shoemaker {
this.lightness
)}%)`}
tabindex=${this.disabled ? null : '0'}
onkeydown=${this.handleGridKeyDown.bind(this)}
/>
@keydown=${this.handleGridKeyDown}
></span>
</div>
<div class="color-picker__controls">
@ -657,8 +652,8 @@ export default class SlColorPicker extends Shoemaker {
<div
part="slider hue-slider"
class="color-picker__hue color-picker__slider"
onmousedown=${this.handleHueDrag.bind(this)}
ontouchstart=${this.handleHueDrag.bind(this)}
@mousedown=${this.handleHueDrag}
@touchstart=${this.handleHueDrag}
>
<span
part="slider-handle"
@ -673,8 +668,8 @@ export default class SlColorPicker extends Shoemaker {
aria-valuemax="360"
aria-valuenow=${Math.round(this.hue)}
tabindex=${this.disabled ? null : 0}
onkeydown=${this.handleHueKeyDown.bind(this)}
/>
@keydown=${this.handleHueKeyDown}
></span>
</div>
${this.opacity
@ -682,19 +677,19 @@ export default class SlColorPicker extends Shoemaker {
<div
part="slider opacity-slider"
class="color-picker__alpha color-picker__slider color-picker__transparent-bg"
onmousedown="${this.handleAlphaDrag.bind(this)}"
ontouchstart="${this.handleAlphaDrag.bind(this)}"
@mousedown="${this.handleAlphaDrag}"
@touchstart="${this.handleAlphaDrag}"
>
<div
class="color-picker__alpha-gradient"
style=${styleMap({
backgroundImage: `linear-gradient(
to right,
hsl(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, 0%) 0%,
to right,
hsl(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, 0%) 0%,
hsl(${this.hue}deg, ${this.saturation}%, ${this.lightness}%) 100%
)`
})}
/>
></div>
<span
part="slider-handle"
class="color-picker__slider-handle"
@ -708,22 +703,21 @@ export default class SlColorPicker extends Shoemaker {
aria-valuemax="100"
aria-valuenow=${Math.round(this.alpha)}
tabindex=${this.disabled ? null : '0'}
onkeydown=${this.handleAlphaKeyDown.bind(this)}
/>
@keydown=${this.handleAlphaKeyDown}
></span>
</div>
`
: ''}
</div>
<button
ref=${(el: HTMLButtonElement) => (this.previewButton = el)}
type="button"
part="preview"
class="color-picker__preview color-picker__transparent-bg"
style=${styleMap({
'--preview-color': `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
})}
onclick=${this.handleCopy.bind(this)}
@click=${this.handleCopy}
>
<sl-icon
name="check"
@ -732,13 +726,12 @@ export default class SlColorPicker extends Shoemaker {
'color-picker__copy-feedback--visible': this.showCopyFeedback,
'color-picker__copy-feedback--dark': this.lightness > 50
})}
/>
></sl-icon>
</button>
</div>
<div class="color-picker__user-input">
<sl-input
ref=${(el: SlInput) => (this.input = el)}
part="input"
size="small"
type="text"
@ -747,15 +740,15 @@ export default class SlColorPicker extends Shoemaker {
autocorrect="off"
autocapitalize="off"
spellcheck="false"
.value=${this.inputValue}
disabled=${this.disabled ? true : null}
onkeydown=${this.handleInputKeyDown.bind(this)}
onsl-change=${this.handleInputChange.bind(this)}
/>
value=${this.inputValue}
?disabled=${this.disabled}
@keydown=${this.handleInputKeyDown}
@sl-change=${this.handleInputChange}
></sl-input>
${!this.noFormatToggle
? html`
<sl-button exportparts="base:format-button" size="small" onclick=${this.handleFormatToggle.bind(this)}>
<sl-button exportparts="base:format-button" size="small" @click=${this.handleFormatToggle}>
${this.setLetterCase(this.format)}
</sl-button>
`
@ -773,11 +766,11 @@ export default class SlColorPicker extends Shoemaker {
tabindex=${this.disabled ? null : '0'}
role="button"
aria-label=${swatch}
onclick=${() => !this.disabled && this.setColor(swatch)}
onkeydown=${(event: KeyboardEvent) =>
@click=${() => !this.disabled && this.setColor(swatch)}
@keydown=${(event: KeyboardEvent) =>
!this.disabled && event.key === 'Enter' && this.setColor(swatch)}
>
<div class="color-picker__swatch-color" style=${styleMap({ backgroundColor: swatch })} />
<div class="color-picker__swatch-color" style=${styleMap({ backgroundColor: swatch })}></div>
</div>
`;
})}
@ -795,15 +788,14 @@ export default class SlColorPicker extends Shoemaker {
// Render as a dropdown
return html`
<sl-dropdown
ref=${(el: SlDropdown) => (this.dropdown = el)}
class="color-dropdown"
aria-disabled=${this.disabled ? 'true' : 'false'}
.containing-element=${this}
hoist=${this.hoist}
onsl-show=${this.handleDropdownShow.bind(this)}
onsl-after-show=${this.handleDropdownAfterShow.bind(this)}
onsl-hide=${this.handleDropdownHide.bind(this)}
onsl-after-hide=${this.handleDropdownAfterHide.bind(this)}
@sl-show=${this.handleDropdownShow}
@sl-after-show=${this.handleDropdownAfterShow}
@sl-hide=${this.handleDropdownHide}
@sl-after-hide=${this.handleDropdownAfterHide}
>
<button
part="trigger"
@ -820,7 +812,7 @@ export default class SlColorPicker extends Shoemaker {
color: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
})}
type="button"
/>
></button>
${colorPicker}
</sl-dropdown>
`;

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./details.scss';
import { focusVisible } from '../../internal/focus-visible';
@ -18,34 +20,40 @@ let id = 0;
* @part summary - The details summary.
* @part summary-icon - The expand/collapse summary icon.
* @part content - The details content.
*
* @emit sl-show - Emitted when the details opens. Calling `event.preventDefault()` will prevent it from being opened.
* @emit after-show - Emitted after the details opens and all transitions are complete.
* @emit sl-hide - Emitted when the details closes. Calling `event.preventDefault()` will prevent it from being closed.
* @emit after-hide - Emitted after the details closes and all transitions are complete.
*/
export default class SlDetails extends Shoemaker {
static tag = 'sl-details';
static props = ['open', 'summary', 'disabled'];
static reflect = ['open', 'disabled'];
static styles = styles;
@customElement('sl-details')
export class SlDetails extends LitElement {
static styles = unsafeCSS(styles);
@query('.details') details: HTMLElement;
@query('.details__header') header: HTMLElement;
@query('.details__body') body: HTMLElement;
private body: HTMLElement;
private componentId = `details-${++id}`;
private details: HTMLElement;
private header: HTMLElement;
private isVisible = false;
/** Indicates whether or not the details is open. You can use this in lieu of the show/hide methods. */
open = false;
@property({ type: Boolean }) open = false;
/** The summary to show in the details header. If you need to display HTML, use the `summary` slot instead. */
summary = '';
@property() summary: string;
/** Disables the details so it can't be toggled. */
disabled = false;
@property({ type: Boolean }) disabled = false;
onReady() {
/** Emitted when the details opens. Calling `event.preventDefault()` will prevent it from being opened. */
@event('sl-show') slShow: EventEmitter<void>;
/** Emitted after the details opens and all transitions are complete. */
@event('after-show') slAfterShow: EventEmitter<void>;
/** Emitted when the details closes. Calling `event.preventDefault()` will prevent it from being closed. */
@event('sl-hide') slHide: EventEmitter<void>;
/** Emitted after the details closes and all transitions are complete. */
@event('after-hide') slAfterHide: EventEmitter<void>;
firstUpdated() {
focusVisible.observe(this.details);
this.body.hidden = !this.open;
@ -56,7 +64,8 @@ export default class SlDetails extends Shoemaker {
}
}
onDisconnect() {
disconnectedCallback() {
super.disconnectedCallback();
focusVisible.unobserve(this.details);
}
@ -67,7 +76,7 @@ export default class SlDetails extends Shoemaker {
return;
}
const slShow = this.emit('sl-show');
const slShow = this.slShow.emit();
if (slShow.defaultPrevented) {
this.open = false;
return;
@ -96,7 +105,7 @@ export default class SlDetails extends Shoemaker {
return;
}
const slHide = this.emit('sl-hide');
const slHide = this.slHide.emit();
if (slHide.defaultPrevented) {
this.open = true;
return;
@ -122,7 +131,7 @@ export default class SlDetails extends Shoemaker {
if (event.propertyName === 'height' && target.classList.contains('details__body')) {
this.body.style.overflow = this.open ? 'visible' : 'hidden';
this.body.style.height = this.open ? 'auto' : '0';
this.open ? this.emit('sl-after-show') : this.emit('sl-after-hide');
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
this.body.hidden = !this.open;
}
}
@ -158,7 +167,6 @@ export default class SlDetails extends Shoemaker {
render() {
return html`
<div
ref=${(el: HTMLElement) => (this.details = el)}
part="base"
class=${classMap({
details: true,
@ -167,7 +175,6 @@ export default class SlDetails extends Shoemaker {
})}
>
<header
ref=${(el: HTMLElement) => (this.header = el)}
part="header"
id=${`${this.componentId}-header`}
class="details__header"
@ -176,23 +183,19 @@ export default class SlDetails extends Shoemaker {
aria-controls=${`${this.componentId}-content`}
aria-disabled=${this.disabled ? 'true' : 'false'}
tabindex=${this.disabled ? '-1' : '0'}
onclick=${this.handleSummaryClick.bind(this)}
onkeydown=${this.handleSummaryKeyDown.bind(this)}
@click=${this.handleSummaryClick}
@keydown=${this.handleSummaryKeyDown}
>
<div part="summary" class="details__summary">
<slot name="summary">${this.summary}</slot>
</div>
<span part="summary-icon" class="details__summary-icon">
<sl-icon name="chevron-right" />
<sl-icon name="chevron-right"></sl-icon>
</span>
</header>
<div
ref=${(el: HTMLElement) => (this.body = el)}
class="details__body"
ontransitionend=${this.handleBodyTransitionEnd.bind(this)}
>
<div class="details__body" @transitionend=${this.handleBodyTransitionEnd}>
<div
part="content"
id=${`${this.componentId}-content`}
@ -200,7 +203,7 @@ export default class SlDetails extends Shoemaker {
role="region"
aria-labelledby=${`${this.componentId}-header`}
>
<slot />
<slot></slot>
</div>
</div>
</div>

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./dialog.scss';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { hasSlot } from '../../internal/slot';
@ -27,47 +29,61 @@ let id = 0;
* @part close-button - The close button.
* @part body - The dialog body.
* @part footer - The dialog footer.
*
* @emit sl-show - Emitted when the dialog opens. Calling `event.preventDefault()` will prevent it from being opened.
* @emit sl-after-show - Emitted after the dialog opens and all transitions are complete.
* @emit sl-hide - Emitted when the dialog closes. Calling `event.preventDefault()` will prevent it from being closed.
* @emit sl-after-hide - Emitted after the dialog closes and all transitions are complete.
* @emit sl-initial-focus - Emitted when the dialog opens and the panel gains focus. Calling `event.preventDefault()`
* will prevent focus and allow you to set it on a different element in the dialog, such as an input or button.
* @emit sl-overlay-dismiss - Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the
* dialog from closing.
*/
export default class SlDialog extends Shoemaker {
static tag = 'sl-dialog';
static props = ['hasFooter', 'isVisible', 'open', 'label', 'noHeader'];
static reflect = ['open'];
static styles = styles;
@customElement('sl-dialog')
export class SlDialog extends LitElement {
static styles = unsafeCSS(styles);
@query('.dialog') dialog: HTMLElement;
@query('.dialog__panel') panel: HTMLElement;
private componentId = `dialog-${++id}`;
private dialog: HTMLElement;
private hasFooter = false;
private isVisible = false;
private modal: Modal;
private panel: HTMLElement;
private willShow = false;
private willHide = false;
@internalProperty() private hasFooter = false;
@internalProperty() private isVisible = false;
/** Indicates whether or not the dialog is open. You can use this in lieu of the show/hide methods. */
open = false;
@property({ type: Boolean, reflect: true }) open = false;
/**
* The dialog's label as displayed in the header. You should always include a relevant label even when using
* `no-header`, as it is required for proper accessibility.
*/
label = '';
@property({ reflect: true }) label = '';
/**
* Disables the header. This will also remove the default close button, so please ensure you provide an easy,
* accessible way for users to dismiss the dialog.
*/
noHeader = false;
@property({ attribute: 'no-header', type: Boolean, reflect: true }) noHeader = false;
/** Emitted when the dialog opens. Calling `event.preventDefault()` will prevent it from being opened. */
@event('sl-show') slShow: EventEmitter<void>;
/** Emitted after the dialog opens and all transitions are complete. */
@event('sl-after-show') slAfterShow: EventEmitter<void>;
/** Emitted when the dialog closes. Calling `event.preventDefault()` will prevent it from being closed. */
@event('sl-hide') slHide: EventEmitter<void>;
/** Emitted after the dialog closes and all transitions are complete. */
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
/**
* Emitted when the dialog opens and the panel gains focus. Calling `event.preventDefault()` will prevent focus and
* allow you to set it on a different element in the dialog, such as an input or button.
*/
@event('sl-initial-focus') slInitialFocus: EventEmitter<void>;
/** Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the dialog from closing. */
@event('sl-overlay-dismiss') slOverlayDismiss: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
onConnect() {
this.modal = new Modal(this, {
onfocusOut: () => this.panel.focus()
});
@ -80,7 +96,8 @@ export default class SlDialog extends Shoemaker {
}
}
onDisconnect() {
disconnectedCallback() {
super.disconnectedCallback();
unlockBodyScrolling(this);
}
@ -90,7 +107,7 @@ export default class SlDialog extends Shoemaker {
return;
}
const slShow = this.emit('sl-show');
const slShow = this.slShow.emit();
if (slShow.defaultPrevented) {
this.open = false;
return;
@ -107,15 +124,15 @@ export default class SlDialog extends Shoemaker {
if (hasPreventScroll) {
// Wait for the next frame before setting initial focus so the dialog is technically visible
requestAnimationFrame(() => {
const slInitialFocus = this.emit('sl-initial-focus');
const slInitialFocus = this.slInitialFocus.emit();
if (!slInitialFocus.defaultPrevented) {
this.panel.focus({ preventScroll: true });
}
});
} else {
// Once Safari supports { preventScroll: true } we can remove this nasty little hack, but until then we need to
// wait for the transition to complete before setting focus, otherwise the panel may render in a buggy way its
// out of view initially.
// wait for the transition to complete before setting focus, otherwise the panel may render in a buggy way
// that's out of view initially.
//
// Fiddle: https://jsfiddle.net/g6buoafq/1/
// Safari: https://bugs.webkit.org/show_bug.cgi?id=178583
@ -123,7 +140,7 @@ export default class SlDialog extends Shoemaker {
this.dialog.addEventListener(
'transitionend',
() => {
const slInitialFocus = this.emit('sl-initial-focus');
const slInitialFocus = this.slInitialFocus.emit();
if (!slInitialFocus.defaultPrevented) {
this.panel.focus();
}
@ -140,7 +157,7 @@ export default class SlDialog extends Shoemaker {
return;
}
const slHide = this.emit('sl-hide');
const slHide = this.slHide.emit();
if (slHide.defaultPrevented) {
this.open = true;
return;
@ -164,8 +181,7 @@ export default class SlDialog extends Shoemaker {
}
handleOverlayClick() {
const slOverlayDismiss = this.emit('sl-overlay-dismiss');
const slOverlayDismiss = this.slOverlayDismiss.emit();
if (!slOverlayDismiss.defaultPrevented) {
this.hide();
}
@ -183,7 +199,7 @@ export default class SlDialog extends Shoemaker {
this.isVisible = this.open;
this.willShow = false;
this.willHide = false;
this.open ? this.emit('sl-after-show') : this.emit('sl-after-hide');
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
}
}
@ -194,7 +210,6 @@ export default class SlDialog extends Shoemaker {
render() {
return html`
<div
ref=${(el: HTMLElement) => (this.dialog = el)}
part="base"
class=${classMap({
dialog: true,
@ -202,13 +217,12 @@ export default class SlDialog extends Shoemaker {
'dialog--visible': this.isVisible,
'dialog--has-footer': this.hasFooter
})}
onkeydown=${this.handleKeyDown.bind(this)}
ontransitionend=${this.handleTransitionEnd.bind(this)}
@keydown=${this.handleKeyDown}
@transitionend=${this.handleTransitionEnd}
>
<div part="overlay" class="dialog__overlay" onclick=${this.handleOverlayClick.bind(this)} tabindex="-1" />
<div part="overlay" class="dialog__overlay" @click=${this.handleOverlayClick} tabindex="-1"></div>
<div
ref=${(el: HTMLElement) => (this.panel = el)}
part="panel"
class="dialog__panel"
role="dialog"
@ -228,18 +242,18 @@ export default class SlDialog extends Shoemaker {
exportparts="base:close-button"
class="dialog__close"
name="x"
onclick="${this.handleCloseClick.bind(this)}"
/>
@click="${this.handleCloseClick}"
></sl-icon-button>
</header>
`
: ''}
<div part="body" class="dialog__body">
<slot />
<slot></slot>
</div>
<footer part="footer" class="dialog__footer">
<slot name="footer" onslotchange=${this.handleSlotChange.bind(this)} />
<slot name="footer" @slotchange=${this.handleSlotChange}></slot>
</footer>
</div>
</div>

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./drawer.scss';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { hasSlot } from '../../internal/slot';
@ -27,56 +29,67 @@ let id = 0;
* @part close-button - The close button.
* @part body - The drawer body.
* @part footer - The drawer footer.
*
* @emit sl-show - Emitted when the drawer opens. Calling `event.preventDefault()` will prevent it from being opened.
* @emit sl-after-show - Emitted after the drawer opens and all transitions are complete.
* @emit sl-hide - Emitted when the drawer closes. Calling `event.preventDefault()` will prevent it from being closed.
* @emit sl-after-hide - Emitted after the drawer closes and all transitions are complete.
* @emit sl-initial-focus - Emitted when the drawer opens and the panel gains focus. Calling `event.preventDefault()`
* will prevent focus and allow you to set it on a different element in the drawer, such as an input or button.
* @emit sl-overlay-dismiss - Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the
* drawer from closing.
*/
export default class SlDrawer extends Shoemaker {
static tag = 'sl-drawer';
static props = ['hasFooter', 'isVisible', 'open', 'label', 'placement', 'contained', 'noHeader'];
static reflect = ['open'];
static styles = styles;
@customElement('sl-drawer')
export class SlDrawer extends LitElement {
static styles = unsafeCSS(styles);
@query('.drawer') drawer: HTMLElement;
@query('.drawer__panel') panel: HTMLElement;
private componentId = `drawer-${++id}`;
private drawer: HTMLElement;
private hasFooter = false;
private isVisible = false;
private modal: Modal;
private panel: HTMLElement;
private willShow = false;
private willHide = false;
@internalProperty() private hasFooter = false;
@internalProperty() private isVisible = false;
/** Indicates whether or not the drawer is open. You can use this in lieu of the show/hide methods. */
open = false;
@property({ type: Boolean, reflect: true }) open = false;
/**
* The drawer's label as displayed in the header. You should always include a relevant label even when using
* `no-header`, as it is required for proper accessibility.
*/
label = '';
@property({ reflect: true }) label = '';
/** The direction from which the drawer will open. */
placement: 'top' | 'right' | 'bottom' | 'left' = 'right';
@property({ reflect: true }) placement: 'top' | 'right' | 'bottom' | 'left' = 'right';
/**
* By default, the drawer slides out of its containing block (usually the viewport). To make the drawer slide out of
* its parent element, set this prop and add `position: relative` to the parent.
*/
contained = false;
@property({ type: Boolean, reflect: true }) contained = false;
/**
* Removes the header. This will also remove the default close button, so please ensure you provide an easy,
* accessible way for users to dismiss the drawer.
*/
noHeader = false;
@property({ attribute: 'no-header', type: Boolean, reflect: true }) noHeader = false;
/** Emitted when the drawer opens. Calling `event.preventDefault()` will prevent it from being opened. */
@event('sl-show') slShow: EventEmitter<void>;
/** Emitted after the drawer opens and all transitions are complete. */
@event('sl-after-show') slAfterShow: EventEmitter<void>;
/** Emitted when the drawer closes. Calling `event.preventDefault()` will prevent it from being closed. */
@event('sl-hide') slHide: EventEmitter<void>;
/** Emitted after the drawer closes and all transitions are complete. */
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
/** Emitted when the drawer opens and the panel gains focus. Calling `event.preventDefault()` will prevent focus and allow you to set it on a different element in the drawer, such as an input or button. */
@event('sl-initial-focus') slInitialFocus: EventEmitter<void>;
/** Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the drawer from closing. */
@event('sl-overlay-dismiss') slOverlayDismiss: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
onConnect() {
this.modal = new Modal(this, {
onfocusOut: () => (this.contained ? null : this.panel.focus())
});
@ -89,7 +102,8 @@ export default class SlDrawer extends Shoemaker {
}
}
onDisconnect() {
disconnectedCallback() {
super.disconnectedCallback();
unlockBodyScrolling(this);
}
@ -99,7 +113,7 @@ export default class SlDrawer extends Shoemaker {
return;
}
const slShow = this.emit('sl-show');
const slShow = this.slShow.emit();
if (slShow.defaultPrevented) {
this.open = false;
return;
@ -119,7 +133,7 @@ export default class SlDrawer extends Shoemaker {
if (hasPreventScroll) {
// Wait for the next frame before setting initial focus so the drawer is technically visible
requestAnimationFrame(() => {
const slInitialFocus = this.emit('sl-initial-focus');
const slInitialFocus = this.slInitialFocus.emit();
if (!slInitialFocus.defaultPrevented) {
this.panel.focus({ preventScroll: true });
}
@ -135,7 +149,7 @@ export default class SlDrawer extends Shoemaker {
this.drawer.addEventListener(
'transitionend',
() => {
const slInitialFocus = this.emit('sl-initial-focus');
const slInitialFocus = this.slInitialFocus.emit();
if (!slInitialFocus.defaultPrevented) {
this.panel.focus();
}
@ -152,7 +166,7 @@ export default class SlDrawer extends Shoemaker {
return;
}
const slHide = this.emit('sl-hide');
const slHide = this.slHide.emit();
if (slHide.defaultPrevented) {
this.open = true;
return;
@ -176,8 +190,7 @@ export default class SlDrawer extends Shoemaker {
}
handleOverlayClick() {
const slOverlayDismiss = this.emit('sl-overlay-dismiss');
const slOverlayDismiss = this.slOverlayDismiss.emit();
if (!slOverlayDismiss.defaultPrevented) {
this.hide();
}
@ -195,14 +208,13 @@ export default class SlDrawer extends Shoemaker {
this.isVisible = this.open;
this.willShow = false;
this.willHide = false;
this.open ? this.emit('sl-after-show') : this.emit('sl-after-hide');
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
}
}
render() {
return html`
<div
ref=${(el: HTMLElement) => (this.drawer = el)}
part="base"
class=${classMap({
drawer: true,
@ -216,13 +228,12 @@ export default class SlDrawer extends Shoemaker {
'drawer--fixed': !this.contained,
'drawer--has-footer': this.hasFooter
})}
onkeydown=${this.handleKeyDown.bind(this)}
ontransitionend=${this.handleTransitionEnd.bind(this)}
@keydown=${this.handleKeyDown}
@transitionend=${this.handleTransitionEnd}
>
<div part="overlay" class="drawer__overlay" onclick=${this.handleOverlayClick.bind(this)} tabindex="-1" />
<div part="overlay" class="drawer__overlay" @click=${this.handleOverlayClick} tabindex="-1"></div>
<div
ref=${(el: HTMLElement) => (this.panel = el)}
part="panel"
class="drawer__panel"
role="dialog"
@ -243,18 +254,18 @@ export default class SlDrawer extends Shoemaker {
exportparts="base:close-button"
class="drawer__close"
name="x"
onclick=${this.handleCloseClick.bind(this)}
/>
@click=${this.handleCloseClick}
></sl-icon-button>
</header>
`
: ''}
<div part="body" class="drawer__body">
<slot />
<slot></slot>
</div>
<footer part="footer" class="drawer__footer">
<slot name="footer" onslotchange=${this.handleSlotChange.bind(this)} />
<slot name="footer" @slotchange=${this.handleSlotChange}></slot>
</footer>
</div>
</div>

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./dropdown.scss';
import { SlMenu, SlMenuItem } from '../../shoelace';
import { scrollIntoView } from '../../internal/scroll';
@ -17,33 +19,27 @@ let id = 0;
* @part base - The component's base wrapper.
* @part trigger - The container that wraps the trigger.
* @part panel - The panel that gets shown when the dropdown is open.
*
* @emit sl-show - Emitted when the dropdown opens. Calling `event.preventDefault()` will prevent it from being opened.
* @emit sl-after-show - Emitted after the dropdown opens and all transitions are complete.
* @emit sl-hide - Emitted when the dropdown closes. Calling `event.preventDefault()` will prevent it from being closed.
* @emit sl-after-hide - Emitted after the dropdown closes and all transitions are complete.
*/
export default class SlDropdown extends Shoemaker {
static tag = 'sl-dropdown';
static props = ['open', 'placement', 'closeOnSelect', 'containingElement', 'distance', 'skidding', 'hoist'];
static reflect = ['open'];
static styles = styles;
@customElement('sl-dropdown')
export class SlDropdown extends LitElement {
static styles = unsafeCSS(styles);
@query('.dropdown__trigger') trigger: HTMLElement;
@query('.dropdown__panel') panel: HTMLElement;
@query('.dropdown__positioner') positioner: HTMLElement;
private componentId = `dropdown-${++id}`;
private isVisible = false;
private panel: HTMLElement;
private positioner: HTMLElement;
private popover: Popover;
private trigger: HTMLElement;
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
open = false;
@property({ type: Boolean }) open = false;
/**
* The preferred placement of the dropdown panel. Note that the actual placement may vary as needed to keep the panel
* inside of the viewport.
*/
placement:
@property() placement:
| 'top'
| 'top-start'
| 'top-end'
@ -58,22 +54,34 @@ export default class SlDropdown extends Shoemaker {
| 'left-end' = 'bottom-start';
/** Determines whether the dropdown should hide when a menu item is selected. */
closeOnSelect = true;
@property({ attribute: 'close-on-select', type: Boolean }) closeOnSelect = true;
/** The dropdown will close when the user interacts outside of this element (e.g. clicking). */
containingElement: HTMLElement;
@property() containingElement: HTMLElement;
/** The distance in pixels from which to offset the panel away from its trigger. */
distance = 2;
@property({ type: Number }) distance = 2;
/** The distance in pixels from which to offset the panel along its trigger. */
skidding = 0;
@property({ type: Number }) skidding = 0;
/**
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
* `overflow: auto|scroll`.
*/
hoist = false;
@property({ type: Boolean }) hoist = false;
/** Emitted when the dropdown opens. Calling `event.preventDefault()` will prevent it from being opened. */
@event('sl-show') slShow: EventEmitter<void>;
/** Emitted after the dropdown opens and all transitions are complete. */
@event('sl-after-show') slAfterShow: EventEmitter<void>;
/** Emitted when the dropdown closes. Calling `event.preventDefault()` will prevent it from being closed. */
@event('sl-hide') slHide: EventEmitter<void>;
/** Emitted after the dropdown closes and all transitions are complete. */
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
handlePopoverOptionsChange() {
this.popover.setOptions({
@ -84,7 +92,8 @@ export default class SlDropdown extends Shoemaker {
});
}
onConnect() {
connectedCallback() {
super.connectedCallback();
this.handleMenuItemActivate = this.handleMenuItemActivate.bind(this);
this.handlePanelSelect = this.handlePanelSelect.bind(this);
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
@ -95,15 +104,15 @@ export default class SlDropdown extends Shoemaker {
}
}
onReady() {
firstUpdated() {
this.popover = new Popover(this.trigger, this.positioner, {
strategy: this.hoist ? 'fixed' : 'absolute',
placement: this.placement,
distance: this.distance,
skidding: this.skidding,
transitionElement: this.panel,
onAfterHide: () => this.emit('sl-after-hide'),
onAfterShow: () => this.emit('sl-after-show'),
onAfterHide: () => this.slAfterHide.emit(),
onAfterShow: () => this.slAfterShow.emit(),
onTransitionEnd: () => {
if (!this.open) {
this.panel.scrollTop = 0;
@ -117,7 +126,8 @@ export default class SlDropdown extends Shoemaker {
}
}
onDisconnect() {
disconnectedCallback() {
super.disconnectedCallback();
this.hide();
this.popover.destroy();
}
@ -294,7 +304,7 @@ export default class SlDropdown extends Shoemaker {
return;
}
const slShow = this.emit('sl-show');
const slShow = this.slShow.emit();
if (slShow.defaultPrevented) {
this.open = false;
return;
@ -317,7 +327,7 @@ export default class SlDropdown extends Shoemaker {
return;
}
const slHide = this.emit('sl-hide');
const slHide = this.slHide.emit();
if (slHide.defaultPrevented) {
this.open = true;
return;
@ -378,19 +388,17 @@ export default class SlDropdown extends Shoemaker {
<span
part="trigger"
class="dropdown__trigger"
ref=${(el: HTMLElement) => (this.trigger = el)}
onclick=${this.handleTriggerClick.bind(this)}
onkeydown=${this.handleTriggerKeyDown.bind(this)}
onkeyup=${this.handleTriggerKeyUp.bind(this)}
@click=${this.handleTriggerClick}
@keydown=${this.handleTriggerKeyDown}
@keyup=${this.handleTriggerKeyUp}
>
<slot name="trigger" onslotchange=${this.handleTriggerSlotChange.bind(this)} />
<slot name="trigger" @slotchange=${this.handleTriggerSlotChange} />
</span>
<!-- Position the panel with a wrapper since the popover makes use of translate. This let's us add transitions
on the panel without interfering with the position. -->
<div ref=${(el: HTMLElement) => (this.positioner = el)} class="dropdown__positioner">
<div class="dropdown__positioner">
<div
ref=${(el: HTMLElement) => (this.panel = el)}
part="panel"
class="dropdown__panel"
role="menu"

Wyświetl plik

@ -1,4 +1,5 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./form.scss';
import {
SlButton,
@ -26,25 +27,30 @@ interface FormControl {
* @slot - The form's content.
*
* @part base - The component's base wrapper.
*
* @emit sl-submit - Emitted when the form is submitted. This event will not be emitted if any form control inside of
* it is in an invalid state, unless the form has the `novalidate` attribute. Note that there is never a need to prevent
* this event, since it doen't send a GET or POST request like native forms. To "prevent" submission, use a conditional
* around the XHR request you use to submit the form's data with. Event details will contain:
* `{ formData: FormData; formControls: HTMLElement[] }`
*/
export default class SlForm extends Shoemaker {
static tag = 'sl-form';
static props = ['novalidate'];
static styles = styles;
@customElement('sl-form')
export class SlForm extends LitElement {
static styles = unsafeCSS(styles);
@query('.form') form: HTMLElement;
private form: HTMLElement;
private formControls: FormControl[];
/** Prevent the form from validating inputs before submitting. */
novalidate = false;
@property({ type: Boolean, reflect: true }) novalidate = false;
/**
* @emit sl-submit - Emitted when the form is submitted. This event will not be emitted if any form control inside of
* it is in an invalid state, unless the form has the `novalidate` attribute. Note that there is never a need to prevent
* this event, since it doen't send a GET or POST request like native forms. To "prevent" submission, use a conditional
* around the XHR request you use to submit the form's data with. Event details will contain:
* `{ formData: FormData; formControls: HTMLElement[] }`
*/
@event('sl-submit') slSubmit: EventEmitter<{ formData: FormData; formControls: HTMLElement[] }>;
connectedCallback() {
super.connectedCallback();
onConnect() {
this.formControls = [
{
tag: 'button',
@ -183,9 +189,6 @@ export default class SlForm extends Shoemaker {
el.name && !el.disabled ? formData.append(el.name, el.value) : null
}
];
this.handleClick = this.handleClick.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
/** Serializes all form controls elements and returns a `FormData` object. */
@ -230,7 +233,7 @@ export default class SlForm extends Shoemaker {
}
}
this.emit('sl-submit', { detail: { formData, formControls } });
this.slSubmit.emit({ detail: { formData, formControls } });
return true;
}
@ -271,15 +274,8 @@ export default class SlForm extends Shoemaker {
render() {
return html`
<div
ref=${(el: HTMLElement) => (this.form = el)}
part="base"
class="form"
role="form"
onclick=${this.handleClick.bind(this)}
onkeydown=${this.handleKeyDown.bind(this)}
>
<slot />
<div part="base" class="form" role="form" @click=${this.handleClick} @keydown=${this.handleKeyDown}>
<slot></slot>
</div>
`;
}

Wyświetl plik

@ -1,22 +1,20 @@
import { Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, property } from 'lit-element';
import { formatBytes } from '../../internal/number';
/**
* @since 2.0
* @status stable
*/
export default class SlFormatBytes extends Shoemaker {
static tag = 'sl-format-bytes';
static props = ['value', 'unit', 'locale'];
@customElement('sl-format-bytes')
export class SlFormatBytes extends LitElement {
/** The number to format in bytes. */
value = 0;
@property({ type: Number }) value = 0;
/** The unit to display. */
unit: 'bytes' | 'bits' = 'bytes';
@property() unit: 'bytes' | 'bits' = 'bytes';
/** The locale to use when formatting the number. */
locale: string;
@property() locale: string;
render() {
return formatBytes(this.value, {

Wyświetl plik

@ -1,65 +1,49 @@
import { Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, property } from 'lit-element';
/**
* @since 2.0
* @status stable
*/
export default class SlFormatDate extends Shoemaker {
static tag = 'sl-format-date';
static props = [
'date',
'locale',
'weekday',
'era',
'year',
'month',
'day',
'hour',
'minute',
'second',
'timeZoneName',
'timeZone',
'hourFormat'
];
@customElement('sl-format-date')
export class SlFormatDate extends LitElement {
/** The date/time to format. If not set, the current date and time will be used. */
date: Date | string = new Date();
@property() date: Date | string = new Date();
/** The locale to use when formatting the date/time. */
locale: string;
@property() locale: string;
/** The format for displaying the weekday. */
weekday: 'narrow' | 'short' | 'long';
@property() weekday: 'narrow' | 'short' | 'long';
/** The format for displaying the era. */
era: 'narrow' | 'short' | 'long';
@property() era: 'narrow' | 'short' | 'long';
/** The format for displaying the year. */
year: 'numeric' | '2-digit';
@property() year: 'numeric' | '2-digit';
/** The format for displaying the month. */
month: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long';
@property() month: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long';
/** The format for displaying the day. */
day: 'numeric' | '2-digit';
@property() day: 'numeric' | '2-digit';
/** The format for displaying the hour. */
hour: 'numeric' | '2-digit';
@property() hour: 'numeric' | '2-digit';
/** The format for displaying the minute. */
minute: 'numeric' | '2-digit';
@property() minute: 'numeric' | '2-digit';
/** The format for displaying the second. */
second: 'numeric' | '2-digit';
@property() second: 'numeric' | '2-digit';
/** The format for displaying the time. */
timeZoneName: 'short' | 'long';
@property({ attribute: 'time-zone-name' }) timeZoneName: 'short' | 'long';
/** The time zone to express the time in. */
timeZone: string;
@property({ attribute: 'time-zone' }) timeZone: string;
/** When set, 24 hour time will always be used. */
hourFormat: 'auto' | '12' | '24' = 'auto';
@property({ attribute: 'hour-format' }) hourFormat: 'auto' | '12' | '24' = 'auto';
render() {
const date = new Date(this.date);

Wyświetl plik

@ -1,57 +1,43 @@
import { Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, property } from 'lit-element';
/**
* @since 2.0
* @status stable
*/
export default class SlFormatNumber extends Shoemaker {
static tag = 'sl-format-number';
static props = [
'value',
'locale',
'type',
'noGrouping',
'currency',
'currencyDisplay',
'minimumIntegerDigits',
'minimumFractionDigits',
'maximumFractionDigits',
'minimumSignificantDigits',
'maximumSignificantDigits'
];
@customElement('sl-format-number')
export class SlFormatNumber extends LitElement {
/** The number to format. */
value = 0;
@property({ type: Number }) value = 0;
/** The locale to use when formatting the number. */
locale: string;
@property() locale: string;
/** The formatting style to use. */
type: 'currency' | 'decimal' | 'percent' = 'decimal';
@property() type: 'currency' | 'decimal' | 'percent' = 'decimal';
/** Turns off grouping separators. */
noGrouping = false;
@property({ type: Boolean }) noGrouping = false;
/** The currency to use when formatting. Must be an ISO 4217 currency code such as `USD` or `EUR`. */
currency = 'USD';
@property() currency = 'USD';
/** How to display the currency. */
currencyDisplay: 'symbol' | 'narrowSymbol' | 'code' | 'name' = 'symbol';
@property({ attribute: 'currency-display' }) currencyDisplay: 'symbol' | 'narrowSymbol' | 'code' | 'name' = 'symbol';
/** The minimum number of integer digits to use. Possible values are 1 - 21. */
minimumIntegerDigits: number;
@property({ attribute: 'minimum-integer-digits', type: Number }) minimumIntegerDigits: number;
/** The minimum number of fraction digits to use. Possible values are 0 - 20. */
minimumFractionDigits: number;
@property({ attribute: 'minimum-fraction-digits', type: Number }) minimumFractionDigits: number;
/** The maximum number of fraction digits to use. Possible values are 0 - 20. */
maximumFractionDigits: number;
@property({ attribute: 'maximum-fraction-digits', type: Number }) maximumFractionDigits: number;
/** The minimum number of significant digits to use. Possible values are 1 - 21. */
minimumSignificantDigits: number;
@property({ attribute: 'minimum-significant-digits', type: Number }) minimumSignificantDigits: number;
/** The maximum number of significant digits to use,. Possible values are 1 - 21. */
maximumSignificantDigits: number;
@property({ attribute: 'maximum-significant-digits', type: Number }) maximumSignificantDigits: number;
render() {
if (isNaN(this.value)) {

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import styles from 'sass:./icon-button.scss';
import { focusVisible } from '../../internal/focus-visible';
@ -10,53 +12,57 @@ import { focusVisible } from '../../internal/focus-visible';
*
* @part base - The component's base wrapper.
*/
export default class SlIconButton extends Shoemaker {
static tag = 'sl-icon-button';
static props = ['name', 'library', 'src', 'label', 'disabled'];
static reflect = ['disabled'];
static styles = styles;
@customElement('sl-icon-button')
export class SlIconButton extends LitElement {
static styles = unsafeCSS(styles);
private button: HTMLButtonElement;
@query('button') button: HTMLButtonElement;
/** The name of the icon to draw. */
name: string;
@property() name: string;
/** The name of a registered custom icon library. */
library: string;
@property() library: string;
/** An external URL of an SVG file. */
src: string;
@property() src: string;
/**
* A description that gets read by screen readers and other assistive devices. For optimal accessibility, you should
* always include a label that describes what the icon button does.
*/
label: string;
@property() label: string;
/** Disables the button. */
disabled = false;
@property({ type: Boolean }) disabled = false;
onReady() {
firstUpdated() {
focusVisible.observe(this.button);
}
disconnectedCallback() {
super.disconnectedCallback();
focusVisible.unobserve(this.button);
}
render() {
return html`
<button
ref=${(el: HTMLButtonElement) => (this.button = el)}
part="base"
class=${classMap({
'icon-button': true,
'icon-button--disabled': this.disabled
})}
?disabled=${this.disabled}
type="button"
aria-label=${this.label}
>
<sl-icon library=${this.library} name=${this.name} src=${this.src} aria-hidden="true" />
<sl-icon
name=${ifDefined(this.name)}
library=${ifDefined(this.library)}
src=${ifDefined(this.src)}
aria-hidden="true"
></sl-icon>
</button>
`;
}

Wyświetl plik

@ -1,4 +1,6 @@
import { html, Hole, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, unsafeCSS } from 'lit-element';
import { unsafeSVG } from 'lit-html/directives/unsafe-svg';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./icon.scss';
import { getIconLibrary, watchIcon, unwatchIcon } from './library';
import { requestIcon } from './request';
@ -10,38 +12,50 @@ const parser = new DOMParser();
* @status stable
*
* @part base - The component's base wrapper.
*
* @emit sl-load - Emitted when the icon has loaded.
* @emit sl-error - Emitted when the icon failed to load. Event details may include: `{ status: number }`
*/
export default class SlIcon extends Shoemaker {
static tag = 'sl-icon';
static props = ['svg', 'name', 'src', 'label', 'library'];
static styles = styles;
@customElement('sl-icon')
export class SlIcon extends LitElement {
static styles = unsafeCSS(styles);
private svg: Hole | string;
@internalProperty() private svg = '';
/** The name of the icon to draw. */
name: string;
@property() name: string;
/** An external URL of an SVG file. */
src: string;
@property() src: string;
/** An alternative description to use for accessibility. If omitted, the name or src will be used to generate it. */
label: string;
@property() label: string;
/** The name of a registered custom icon library. */
library = 'default';
@property() library = 'default';
onConnect() {
/** Emitted when the icon has loaded. */
@event('sl-load') slLoad: EventEmitter<void>;
/** Emitted when the icon failed to load. Event details may include: `{ status: number }` */
@event('sl-error') slError: EventEmitter<{ status: number }>;
connectedCallback() {
super.connectedCallback();
watchIcon(this);
}
onReady() {
firstUpdated() {
this.setIcon();
}
onDisconnect() {
update(changedProps: Map<string, any>) {
if (['name', 'src', 'library'].find(prop => changedProps.has(prop))) {
this.setIcon();
}
super.update(changedProps);
}
disconnectedCallback() {
super.disconnectedCallback();
unwatchIcon(this);
}
@ -84,18 +98,18 @@ export default class SlIcon extends Shoemaker {
library.mutator(svgEl);
}
this.svg = html([svgEl.outerHTML] as any);
this.emit('sl-load');
this.svg = svgEl.outerHTML;
this.slLoad.emit();
} else {
this.svg = '';
this.emit('sl-error', { detail: { status: file.status } });
this.slError.emit({ detail: { status: file.status } });
}
} else {
this.svg = '';
this.emit('sl-error', { detail: { status: file.status } });
this.slError.emit({ detail: { status: file.status } });
}
} catch {
this.emit('sl-error');
this.slError.emit({ detail: { status: -1 } });
}
} else if (this.svg) {
// If we can't resolve a URL and an icon was previously set, remove it
@ -103,23 +117,11 @@ export default class SlIcon extends Shoemaker {
}
}
watchName() {
this.setIcon();
}
watchSrc() {
this.setIcon();
}
watchLibrary() {
this.setIcon();
}
handleChange() {
this.setIcon();
}
render() {
return html` <div part="base" class="icon" role="img" aria-label=${this.getLabel()}>${this.svg}</div>`;
return html` <div part="base" class="icon" role="img" aria-label=${this.getLabel()}>${unsafeSVG(this.svg)}</div>`;
}
}

Wyświetl plik

@ -1,4 +1,4 @@
import Icon from './icon';
import { SlIcon } from './icon';
import { getBasePath } from '../../utilities/base-path';
export type IconLibraryResolver = (name: string) => string;
@ -16,13 +16,13 @@ let registry: IconLibraryRegistry[] = [
resolver: name => `${getBasePath()}/assets/icons/${name}.svg`
}
];
let watchedIcons: Icon[] = [];
let watchedIcons: SlIcon[] = [];
export function watchIcon(icon: Icon) {
export function watchIcon(icon: SlIcon) {
watchedIcons.push(icon);
}
export function unwatchIcon(icon: Icon) {
export function unwatchIcon(icon: SlIcon) {
watchedIcons = watchedIcons.filter(el => el !== icon);
}

Wyświetl plik

@ -1,5 +1,7 @@
import { html, styleMap, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import { styleMap } from 'lit-html/directives/style-map';
import styles from 'sass:./image-comparer.scss';
import { event, EventEmitter } from '../../internal/event';
import { clamp } from '../../internal/math';
/**
@ -17,19 +19,26 @@ import { clamp } from '../../internal/math';
* @part after - The container that holds the "after" image.
* @part divider - The divider that separates the images.
* @part handle - The handle that the user drags to expose the after image.
*
* @emit sl-change - Emitted when the slider position changes.
*/
export default class SlImageComparer extends Shoemaker {
static tag = 'sl-image-comparer';
static props = ['position'];
static styles = styles;
@customElement('sl-image-comparer')
export class SlImageComparer extends LitElement {
static styles = unsafeCSS(styles);
private base: HTMLElement;
private handle: HTMLElement;
@query('.image-comparer') base: HTMLElement;
@query('.image-comparer__handle') handle: HTMLElement;
/** The position of the divider as a percentage. */
position = 50;
@property({ type: Number, reflect: true }) position = 50;
@event('sl-change') slChange: EventEmitter<void>;
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('position')) {
this.slChange.emit();
}
}
handleDrag(event: any) {
const { width } = this.base.getBoundingClientRect();
@ -66,7 +75,7 @@ export default class SlImageComparer extends Shoemaker {
event.preventDefault();
drag(event, this.base, x => {
this.position = clamp((x / width) * 100, 0, 100);
this.position = Number(clamp((x / width) * 100, 0, 100).toFixed(2));
});
}
@ -87,51 +96,41 @@ export default class SlImageComparer extends Shoemaker {
}
}
watchPosition() {
this.emit('sl-change');
}
render() {
return html`
<div
ref=${(el: HTMLElement) => (this.base = el)}
part="base"
class="image-comparer"
onkeydown=${this.handleKeyDown.bind(this)}
>
<div part="base" class="image-comparer" @keydown=${this.handleKeyDown}>
<div class="image-comparer__image">
<div part="before" class="image-comparer__before">
<slot name="before" />
<slot name="before"></slot>
</div>
<div
part="after"
class="image-comparer__after"
style="${styleMap({ clipPath: `inset(0 ${100 - this.position}% 0 0)` })}"
style=${styleMap({ clipPath: `inset(0 ${100 - this.position}% 0 0)` })}
>
<slot name="after" />
<slot name="after"></slot>
</div>
</div>
<div
part="divider"
class="image-comparer__divider"
style="${styleMap({ left: this.position + '%' })}"
onmousedown=${this.handleDrag.bind(this)}
ontouchstart=${this.handleDrag.bind(this)}
style=${styleMap({ left: this.position + '%' })}
@mousedown=${this.handleDrag}
@touchstart=${this.handleDrag}
>
<div
ref=${(el: HTMLElement) => (this.handle = el)}
part="handle"
class="image-comparer__handle"
role="scrollbar"
aria-valuenow="${this.position}"
aria-valuenow=${this.position}
aria-valuemin="0"
aria-valuemax="100"
tabindex="0"
>
<slot name="handle-icon">
<sl-icon class="image-comparer__handle-icon" name="grip-vertical" />
<sl-icon class="image-comparer__handle-icon" name="grip-vertical"></sl-icon>
</slot>
</div>
</div>

Wyświetl plik

@ -1,38 +1,45 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, unsafeCSS } from 'lit-element';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./include.scss';
import { requestInclude } from './request';
/**
* @since 2.0
* @status stable
*
* @emit sl-load - Emitted when the included file is loaded.
* @emit sl-error - Emitted when the included file fails to load due to an error. Event details will include: `
* { status: number}`
*/
export default class SlInclude extends Shoemaker {
static tag = 'sl-include';
static props = ['html', 'src', 'mode', ' allowScripts'];
static styles = styles;
@customElement('sl-include')
export class SlInclude extends LitElement {
static styles = unsafeCSS(styles);
/** The location of the HTML file to include. */
src: string;
@property() src: string;
/** The fetch mode to use. */
mode: 'cors' | 'no-cors' | 'same-origin' = 'cors';
@property() mode: 'cors' | 'no-cors' | 'same-origin' = 'cors';
/**
* Allows included scripts to be executed. You must ensure the content you're including is trusted, otherwise this
* option can lead to XSS vulnerabilities in your app!
*/
allowScripts = false;
@property({ attribute: 'allow-scripts', type: Boolean }) allowScripts = false;
watchSrc() {
/** Emitted when the included file is loaded. */
@event('sl-load') slLoad: EventEmitter<void>;
/** Emitted when the included file fails to load due to an error. */
@event('sl-error') slError: EventEmitter<{ status: number }>;
connectedCallback() {
super.connectedCallback();
this.loadSource();
}
onConnect() {
this.loadSource();
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('src')) {
this.loadSource();
}
}
executeScript(script: HTMLScriptElement) {
@ -58,7 +65,7 @@ export default class SlInclude extends Shoemaker {
}
if (!file.ok) {
this.emit('sl-error', { detail: { status: file.status } });
this.slError.emit({ detail: { status: file.status } });
return;
}
@ -68,13 +75,13 @@ export default class SlInclude extends Shoemaker {
[...this.querySelectorAll('script')].map(script => this.executeScript(script));
}
this.emit('sl-load');
this.slLoad.emit();
} catch {
this.emit('sl-error');
this.slError.emit({ detail: { status: -1 } });
}
}
render() {
return html`<slot />`;
return html`<slot></slot>`;
}
}

Wyświetl plik

@ -1,4 +1,7 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { ifDefined } from 'lit-html/directives/if-defined';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./input.scss';
import { renderFormControl } from '../../internal/form-control';
import { hasSlot } from '../../internal/slot';
@ -28,148 +31,143 @@ let id = 0;
* @part password-toggle-button - The password toggle button.
* @part suffix - The input suffix container.
* @part help-text - The input help text.
*
* @emit sl-change - Emitted when the control's value changes.
* @emit sl-clear - Emitted when the clear button is activated.
* @emit sl-input - Emitted when the control receives input.
* @emit sl-focus - Emitted when the control gains focus.
* @emit sl-blur - Emitted when the control loses focus.
*/
export default class SlInput extends Shoemaker {
static tag = 'sl-input';
static props = [
'hasFocus',
'hasHelpTextSlot',
'hasLabelSlot',
'isPasswordVisible',
'type',
'size',
'name',
'value',
'pill',
'label',
'helpText',
'clearable',
'togglePassword',
'placeholder',
'disabled',
'readonly',
'minlength',
'maxlength',
'min',
'max',
'step',
'pattern',
'required',
'invalid',
'autocapitalize',
'autocorrect',
'autocomplete',
'autofocus',
'spellcheck',
'inputmode'
];
static reflect = ['size', 'pill', 'disabled', 'readonly', 'invalid'];
static styles = styles;
@customElement('sl-input')
export class SlInput extends LitElement {
static styles = unsafeCSS(styles);
@query('.input__control') input: HTMLInputElement;
private hasFocus = false;
private hasHelpTextSlot = false;
private hasLabelSlot = false;
private helpTextId = `input-help-text-${id}`;
private input: HTMLInputElement;
private inputId = `input-${++id}`;
private isPasswordVisible = false;
private labelId = `input-label-${id}`;
@internalProperty() private hasFocus = false;
@internalProperty() private hasHelpTextSlot = false;
@internalProperty() private hasLabelSlot = false;
@internalProperty() private isPasswordVisible = false;
/** The input's type. */
type: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' = 'text';
@property({ reflect: true }) type: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' = 'text';
/** The input's size. */
size: 'small' | 'medium' | 'large' = 'medium';
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
/** The input's name attribute. */
name = '';
@property() name: string;
/** The input's value attribute. */
value = '';
@property() value = '';
/** Draws a pill-style input with rounded edges. */
pill = false;
@property({ type: Boolean, reflect: true }) pill = false;
/** The input's label. Alternatively, you can use the label slot. */
label = '';
@property() label: string;
/** The input's help text. Alternatively, you can use the help-text slot. */
helpText = '';
@property({ attribute: 'help-text' }) helpText: string;
/** Adds a clear button when the input is populated. */
clearable = false;
@property({ type: Boolean, reflect: true }) clearable = false;
/** Adds a password toggle button to password inputs. */
togglePassword = false;
@property({ attribute: 'toggle-password', type: Boolean, reflect: true }) togglePassword = false;
/** The input's placeholder text. */
placeholder = '';
@property() placeholder = '';
/** Disables the input. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** Makes the input readonly. */
readonly = false;
@property({ type: Boolean, reflect: true }) readonly = false;
/** The minimum length of input that will be considered valid. */
minlength: number;
@property({ type: Number }) minlength: number;
/** The maximum length of input that will be considered valid. */
maxlength: number;
@property({ type: Number }) maxlength: number;
/** The input's minimum value. */
min: number | string;
@property() min: number | string;
/** The input's maximum value. */
max: number | string;
@property() max: number | string;
/** The input's step attribute. */
step: number;
@property({ type: Number }) step: number;
/** A pattern to validate input against. */
pattern: string;
@property() pattern: string;
/** Makes the input a required field. */
required = false;
@property({ type: Boolean, reflect: true }) required = false;
/**
* This will be true when the control is in an invalid state. Validity is determined by props such as `type`,
* `required`, `minlength`, `maxlength`, and `pattern` using the browser's constraint validation API.
*/
invalid = false;
@property({ type: Boolean, reflect: true }) invalid = false;
/** The input's autocaptialize attribute. */
autocapitalize: string;
@property() autocapitalize: string;
/** The input's autocorrect attribute. */
autocorrect: string;
@property() autocorrect: string;
/** The input's autocomplete attribute. */
autocomplete: string;
@property() autocomplete: string;
/** The input's autofocus attribute. */
autofocus: boolean;
@property({ type: Boolean }) autofocus: boolean;
/** Enables spell checking on the input. */
spellcheck: boolean;
@property({ type: Boolean }) spellcheck: boolean;
/** The input's inputmode attribute. */
inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
@property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
onConnect() {
/** Emitted when the control's value changes. */
@event('sl-change') slChange: EventEmitter<void>;
/** Emitted when the clear button is activated. */
@event('sl-clear') slClear: EventEmitter<void>;
/** Emitted when the control receives input. */
@event('sl-input') slInput: EventEmitter<void>;
/** Emitted when the control gains focus. */
@event('sl-focus') slFocus: EventEmitter<void>;
/** Emitted when the control loses focus. */
@event('sl-blur') slBlur: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
this.handleSlotChange = this.handleSlotChange.bind(this);
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
}
firstUpdated() {
this.handleSlotChange();
}
onDisconnect() {
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('helpText') || changedProps.has('label')) {
this.handleSlotChange();
}
if (changedProps.has('value')) {
this.invalid = !this.input.checkValidity();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
}
@ -208,8 +206,8 @@ export default class SlInput extends Shoemaker {
if (this.value !== this.input.value) {
this.value = this.input.value;
this.emit('sl-input');
this.emit('sl-change');
this.slInput.emit();
this.slChange.emit();
}
}
@ -226,12 +224,12 @@ export default class SlInput extends Shoemaker {
handleChange() {
this.value = this.input.value;
this.emit('sl-change');
this.slChange.emit();
}
handleInput() {
this.value = this.input.value;
this.emit('sl-input');
this.slInput.emit();
}
handleInvalid() {
@ -240,19 +238,19 @@ export default class SlInput extends Shoemaker {
handleBlur() {
this.hasFocus = false;
this.emit('sl-blur');
this.slBlur.emit();
}
handleFocus() {
this.hasFocus = true;
this.emit('sl-focus');
this.slFocus.emit();
}
handleClearClick(event: MouseEvent) {
this.value = '';
this.emit('sl-clear');
this.emit('sl-input');
this.emit('sl-change');
this.slClear.emit();
this.slInput.emit();
this.slChange.emit();
this.input.focus();
event.stopPropagation();
@ -267,18 +265,6 @@ export default class SlInput extends Shoemaker {
this.hasLabelSlot = hasSlot(this, 'label');
}
watchHelpText() {
this.handleSlotChange();
}
watchLabel() {
this.handleSlotChange();
}
watchValue() {
this.invalid = !this.input.checkValidity();
}
render() {
return renderFormControl(
{
@ -311,41 +297,40 @@ export default class SlInput extends Shoemaker {
})}
>
<span part="prefix" class="input__prefix">
<slot name="prefix" />
<slot name="prefix"></slot>
</span>
<input
part="input"
ref=${(el: HTMLInputElement) => (this.input = el)}
id=${this.inputId}
class="input__control"
type=${this.type === 'password' && this.isPasswordVisible ? 'text' : this.type}
name=${this.name}
placeholder=${this.placeholder}
disabled=${this.disabled ? true : null}
readonly=${this.readonly ? true : null}
minlength=${this.minlength}
maxlength=${this.maxlength}
min=${this.min}
max=${this.max}
step=${this.step}
name=${ifDefined(this.name)}
.value=${this.value}
autocapitalize=${this.autocapitalize}
autocomplete=${this.autocomplete}
autocorrect=${this.autocorrect}
autofocus=${this.autofocus}
spellcheck=${this.spellcheck}
pattern=${this.pattern}
required=${this.required ? true : null}
inputmode=${this.inputmode}
?disabled=${this.disabled}
?readonly=${this.readonly}
?required=${this.required}
placeholder=${ifDefined(this.placeholder)}
minlength=${ifDefined(this.minlength)}
maxlength=${ifDefined(this.maxlength)}
min=${ifDefined(this.min)}
max=${ifDefined(this.max)}
step=${ifDefined(this.step)}
autocapitalize=${ifDefined(this.autocapitalize)}
autocomplete=${ifDefined(this.autocomplete)}
autocorrect=${ifDefined(this.autocorrect)}
autofocus=${ifDefined(this.autofocus)}
spellcheck=${ifDefined(this.spellcheck)}
pattern=${ifDefined(this.pattern)}
inputmode=${ifDefined(this.inputmode)}
aria-labelledby=${this.labelId}
aria-describedby=${this.helpTextId}
aria-invalid=${this.invalid ? 'true' : 'false'}
onchange=${this.handleChange.bind(this)}
oninput=${this.handleInput.bind(this)}
oninvalid=${this.handleInvalid.bind(this)}
onfocus=${this.handleFocus.bind(this)}
onblur=${this.handleBlur.bind(this)}
@change=${this.handleChange}
@input=${this.handleInput}
@invalid=${this.handleInvalid}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
/>
${this.clearable && this.value.length > 0
@ -354,11 +339,11 @@ export default class SlInput extends Shoemaker {
part="clear-button"
class="input__clear"
type="button"
onclick=${this.handleClearClick.bind(this)}
@click=${this.handleClearClick}
tabindex="-1"
>
<slot name="clear-icon">
<sl-icon name="x-circle" />
<sl-icon name="x-circle"></sl-icon>
</slot>
</button>
`
@ -369,19 +354,19 @@ export default class SlInput extends Shoemaker {
part="password-toggle-button"
class="input__password-toggle"
type="button"
onclick=${this.handlePasswordToggle.bind(this)}
@click=${this.handlePasswordToggle}
tabindex="-1"
>
${this.isPasswordVisible
? html`
<slot name="show-password-icon">
<sl-icon name="eye-slash" />
<sl-icon name="eye-slash"></sl-icon>
</slot>
`
: html`
<slot name="hide-password-icon">
${' '}
<sl-icon name="eye" />
<sl-icon name="eye"></sl-icon>
</slot>
`}
</button>
@ -389,7 +374,7 @@ export default class SlInput extends Shoemaker {
: ''}
<span part="suffix" class="input__suffix">
<slot name="suffix" />
<slot name="suffix"></slot>
</span>
</div>
`

Wyświetl plik

@ -1,4 +1,4 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, unsafeCSS } from 'lit-element';
import styles from 'sass:./menu-divider.scss';
/**
@ -9,11 +9,11 @@ import styles from 'sass:./menu-divider.scss';
*
* @part base - The component's base wrapper.
*/
export default class SlMenuDivider extends Shoemaker {
static tag = 'sl-menu-divider';
static styles = styles;
@customElement('sl-menu-divider')
export class SlMenuDivider extends LitElement {
static styles = unsafeCSS(styles);
render() {
return html` <div part="base" class="menu-divider" role="separator" aria-hidden="true" /> `;
return html` <div part="base" class="menu-divider" role="separator" aria-hidden="true"></div> `;
}
}

Wyświetl plik

@ -1,5 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import styles from 'sass:./menu-item.scss';
import { classMap } from 'lit-html/directives/class-map';
/**
* @since 2.0
@ -17,23 +18,22 @@ import styles from 'sass:./menu-item.scss';
* @part label - The menu item label.
* @part suffix - The suffix container.
*/
export default class SlMenuItem extends Shoemaker {
static tag = 'sl-menu-item';
static props = ['hasFocus', 'checked', 'value', 'disabled'];
static reflect = ['checked', 'disabled'];
static styles = styles;
@customElement('sl-menu-item')
export class SlMenuItem extends LitElement {
static styles = unsafeCSS(styles);
private hasFocus = false;
private menuItem: HTMLElement;
@query('.menu-item') menuItem: HTMLElement;
@internalProperty() private hasFocus = false;
/** Draws the item in a checked state. */
checked = false;
@property({ type: Boolean, reflect: true }) checked = false;
/** A unique value to store in the menu item. This can be used as a way to identify menu items when selected. */
value = '';
@property() value = '';
/** Draws the menu item in a disabled state. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** Sets focus on the button. */
setFocus(options?: FocusOptions) {
@ -64,7 +64,6 @@ export default class SlMenuItem extends Shoemaker {
render() {
return html`
<div
ref=${(el: HTMLElement) => (this.menuItem = el)}
part="base"
class=${classMap({
'menu-item': true,
@ -76,25 +75,25 @@ export default class SlMenuItem extends Shoemaker {
aria-disabled=${this.disabled ? 'true' : 'false'}
aria-checked=${this.checked ? 'true' : 'false'}
tabindex=${!this.disabled ? '0' : null}
onfocus=${this.handleFocus.bind(this)}
onblur=${this.handleBlur.bind(this)}
onmouseenter=${this.handleMouseEnter.bind(this)}
onmouseleave=${this.handleMouseLeave.bind(this)}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
@mouseenter=${this.handleMouseEnter}
@mouseleave=${this.handleMouseLeave}
>
<span part="checked-icon" class="menu-item__check">
<sl-icon name="check2" aria-hidden="true" />
<sl-icon name="check2" aria-hidden="true"></sl-icon>
</span>
<span part="prefix" class="menu-item__prefix">
<slot name="prefix" />
<slot name="prefix"></slot>
</span>
<span part="label" class="menu-item__label">
<slot />
<slot></slot>
</span>
<span part="suffix" class="menu-item__suffix">
<slot name="suffix" />
<slot name="suffix"></slot>
</span>
</div>
`;

Wyświetl plik

@ -1,4 +1,4 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, unsafeCSS } from 'lit-element';
import styles from 'sass:./menu-label.scss';
/**
@ -11,14 +11,14 @@ import styles from 'sass:./menu-label.scss';
*
* @part base - The component's base wrapper.
*/
export default class SlMenuLabel extends Shoemaker {
static tag = 'sl-menu-label';
static styles = styles;
@customElement('sl-menu-label')
export class SlMenuLabel extends LitElement {
static styles = unsafeCSS(styles);
render() {
return html`
<div part="base" class="menu-label">
<slot />
<slot></slot>
</div>
`;
}

Wyświetl plik

@ -1,4 +1,5 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, query, unsafeCSS } from 'lit-element';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./menu.scss';
import { SlMenuItem } from '../../shoelace';
import { getTextContent } from '../../internal/slot';
@ -10,17 +11,19 @@ import { getTextContent } from '../../internal/slot';
* @slot - The menu's content, including menu items, menu dividers, and menu labels.
*
* @part base - The component's base wrapper.
*
* @emit sl-select - Emitted when a menu item is selected. Event details will contain: `{ item: SlMenuItem }`
*/
export default class SlMenu extends Shoemaker {
static tag = 'sl-menu';
static styles = styles;
@customElement('sl-menu')
export class SlMenu extends LitElement {
static styles = unsafeCSS(styles);
@query('.menu') menu: HTMLElement;
private menu: HTMLElement;
private typeToSelectString = '';
private typeToSelectTimeout: any;
/** Emitted when a menu item is selected. */
@event('sl-select') slSelect: EventEmitter<{ item: SlMenuItem }>;
/**
* Initiates type-to-select logic, which automatically selects an option based on what the user is currently typing.
* The key passed will be appended to the internal query and the selection will be updated. After a brief period, the
@ -62,7 +65,7 @@ export default class SlMenu extends Shoemaker {
const item = target.closest('sl-menu-item') as SlMenuItem;
if (item && !item.disabled) {
this.emit('sl-select', { detail: { item } });
this.slSelect.emit({ detail: { item } });
}
}
@ -73,7 +76,7 @@ export default class SlMenu extends Shoemaker {
event.preventDefault();
if (item) {
this.emit('sl-select', { detail: { item } });
this.slSelect.emit({ detail: { item } });
}
}
@ -115,16 +118,8 @@ export default class SlMenu extends Shoemaker {
render() {
return html`
<div
ref=${(el: HTMLElement) => (this.menu = el)}
part="base"
class="menu"
role="menu"
onclick=${this.handleClick.bind(this)}
onkeydown=${this.handleKeyDown.bind(this)}
tabindex="0"
>
<slot />
<div part="base" class="menu" role="menu" @click=${this.handleClick} @keydown=${this.handleKeyDown} tabindex="0">
<slot></slot>
</div>
`;
}

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, styleMap, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { styleMap } from 'lit-html/directives/style-map';
import styles from 'sass:./progress-bar.scss';
/**
@ -11,16 +13,15 @@ import styles from 'sass:./progress-bar.scss';
* @part indicator - The progress bar indicator.
* @part label - The progress bar label.
*/
export default class SlProgressBar extends Shoemaker {
static tag = 'sl-progress-bar';
static props = ['percentage', 'indeterminate'];
static styles = styles;
@customElement('sl-progress-bar')
export class SlProgressBar extends LitElement {
static styles = unsafeCSS(styles);
/** The progress bar's percentage, 0 to 100. */
percentage = 0;
@property({ type: Number, reflect: true }) percentage = 0;
/** When true, percentage is ignored, the label is hidden, and the progress bar is drawn in an indeterminate state. */
indeterminate = false;
@property({ type: Boolean, reflect: true }) indeterminate = false;
render() {
return html`
@ -39,7 +40,7 @@ export default class SlProgressBar extends Shoemaker {
${!this.indeterminate
? html`
<span part="label" class="progress-bar__label">
<slot />
<slot></slot>
</span>
`
: ''}

Wyświetl plik

@ -1,4 +1,4 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import styles from 'sass:./progress-ring.scss';
/**
@ -10,27 +10,30 @@ import styles from 'sass:./progress-ring.scss';
* @part base - The component's base wrapper.
* @part label - The progress ring label.
*/
export default class SlProgressRing extends Shoemaker {
static tag = 'sl-progress-ring';
static props = ['size', 'strokeWidth', 'percentage'];
static styles = styles;
@customElement('sl-progress-ring')
export class SlProgressRing extends LitElement {
static styles = unsafeCSS(styles);
private indicator: SVGCircleElement;
@query('.progress-ring__indicator') indicator: SVGCircleElement;
/** The size of the progress ring in pixels. */
size = 128;
@property({ type: Number }) size = 128;
/** The stroke width of the progress ring in pixels. */
strokeWidth = 4;
@property({ type: Number }) strokeWidth = 4;
/** The current progress percentage, 0 - 100. */
percentage: number;
@property({ type: Number, reflect: true }) percentage: number;
watchPercentage() {
this.updateProgress();
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('percentage')) {
this.updateProgress();
}
}
onReady() {
firstUpdated() {
this.updateProgress();
}
@ -55,10 +58,9 @@ export default class SlProgressRing extends Shoemaker {
r=${this.size / 2 - this.strokeWidth * 2}
cx=${this.size / 2}
cy=${this.size / 2}
/>
></circle>
<circle
ref=${(el: SVGCircleElement) => (this.indicator = el)}
class="progress-ring__indicator"
stroke-width="${this.strokeWidth}"
stroke-linecap="round"
@ -66,11 +68,11 @@ export default class SlProgressRing extends Shoemaker {
r=${this.size / 2 - this.strokeWidth * 2}
cx=${this.size / 2}
cy=${this.size / 2}
/>
></circle>
</svg>
<span part="label" class="progress-ring__label">
<slot />
<slot></slot>
</span>
</div>
`;

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./radio.scss';
let id = 0;
@ -13,39 +15,56 @@ let id = 0;
* @part control - The radio control.
* @part checked-icon - The container the wraps the checked icon.
* @part label - The radio label.
*
* @emit sl-blur - Emitted when the control loses focus.
* @emit sl-change - Emitted when the control's checked state changes.
* @emit sl-focus - Emitted when the control gains focus.
*/
export default class SlRadio extends Shoemaker {
static tag = 'sl-radio';
static props = ['hasFocus', 'name', 'value', 'disabled', 'checked', 'invalid'];
static reflect = ['checked', 'invalid'];
static styles = styles;
@customElement('sl-radio')
export class SlRadio extends LitElement {
static styles = unsafeCSS(styles);
@query('input[type="radio"]') input: HTMLInputElement;
private hasFocus = false;
private inputId = `radio-${++id}`;
private labelId = `radio-label-${id}`;
private input: HTMLInputElement;
@internalProperty() private hasFocus = false;
/** The radio's name attribute. */
name: string;
@property() name: string;
/** The radio's value attribute. */
value: string;
@property() value: string;
/** Disables the radio. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** Draws the radio in a checked state. */
checked = false;
@property({ type: Boolean, reflect: true }) checked = false;
/**
* This will be true when the control is in an invalid state. Validity in range inputs is determined by the message
* provided by the `setCustomValidity` method.
*/
invalid = false;
@property({ type: Boolean, reflect: true }) invalid = false;
/** Emitted when the control loses focus. */
@event('sl-blur') slBlur: EventEmitter<void>;
/** Emitted when the control's checked state changes. */
@event('sl-change') slChange: EventEmitter<void>;
/** Emitted when the control gains focus. */
@event('sl-focus') slFocus: EventEmitter<void>;
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('checked')) {
if (this.checked) {
this.getSiblingRadios().map(radio => (radio.checked = false));
}
this.input.checked = this.checked;
this.slChange.emit();
}
}
/** Sets focus on the radio. */
setFocus(options?: FocusOptions) {
@ -81,17 +100,17 @@ export default class SlRadio extends Shoemaker {
}
handleClick() {
this.checked = this.input.checked;
this.checked = true;
}
handleBlur() {
this.hasFocus = false;
this.emit('sl-blur');
this.slBlur.emit();
}
handleFocus() {
this.hasFocus = true;
this.emit('sl-focus');
this.slFocus.emit();
}
handleKeyDown(event: KeyboardEvent) {
@ -116,14 +135,6 @@ export default class SlRadio extends Shoemaker {
this.input.focus();
}
watchChecked() {
if (this.checked) {
this.getSiblingRadios().map(radio => (radio.checked = false));
}
this.input.checked = this.checked;
this.emit('sl-change');
}
render() {
return html`
<label
@ -135,8 +146,8 @@ export default class SlRadio extends Shoemaker {
'radio--focused': this.hasFocus
})}
for=${this.inputId}
onkeydown=${this.handleKeyDown.bind(this)}
onmousedown=${this.handleMouseDown.bind(this)}
@keydown=${this.handleKeyDown}
@mousedown=${this.handleMouseDown}
>
<span part="control" class="radio__control">
<span part="checked-icon" class="radio__icon">
@ -150,24 +161,23 @@ export default class SlRadio extends Shoemaker {
</span>
<input
ref=${(el: HTMLInputElement) => (this.input = el)}
id=${this.inputId}
type="radio"
name=${this.name}
.value=${this.value}
checked=${this.checked ? true : null}
disabled=${this.disabled ? true : null}
?checked=${this.checked}
?disabled=${this.disabled}
role="radio"
aria-checked=${this.checked ? 'true' : 'false'}
aria-labelledby=${this.labelId}
onclick=${this.handleClick.bind(this)}
onblur=${this.handleBlur.bind(this)}
onfocus=${this.handleFocus.bind(this)}
@click=${this.handleClick}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
/>
</span>
<span part="label" id=${this.labelId} class="radio__label">
<slot />
<slot></slot>
</span>
</label>
`;

Wyświetl plik

@ -122,7 +122,7 @@
}
}
// Tooltip
// Tooltip output
.range__tooltip {
position: absolute;
z-index: var(--sl-z-index-tooltip);

Wyświetl plik

@ -1,4 +1,7 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./range.scss';
import { renderFormControl } from '../../internal/form-control';
import { hasSlot } from '../../internal/slot';
@ -15,81 +18,71 @@ let id = 0;
* @part base - The component's base wrapper.
* @part input - The native range input.
* @part tooltip - The range tooltip.
*
* @emit sl-change - Emitted when the control's value changes.
* @emit sl-blur - Emitted when the control loses focus.
* @emit sl-focus - Emitted when the control gains focus.
*/
export default class SlRange extends Shoemaker {
static tag = 'sl-range';
static props = [
'hasFocus',
'hasHelpTextSlot',
'hasLabelSlot',
'hasTooltip',
'name',
'value',
'label',
'helpText',
'disabled',
'invalid',
'min',
'max',
'step',
'tooltip',
'tooltipFormatter'
];
static reflect = ['disabled', 'invalid'];
static styles = styles;
@customElement('sl-range')
export class SlRange extends LitElement {
static styles = unsafeCSS(styles);
@query('.range__control') input: HTMLInputElement;
@query('.range__tooltip') output: HTMLOutputElement;
private hasFocus = false;
private hasHelpTextSlot = false;
private hasLabelSlot = false;
private hasTooltip = false;
private helpTextId = `input-help-text-${id}`;
private input: HTMLInputElement;
private inputId = `input-${++id}`;
private labelId = `input-label-${id}`;
private output: HTMLElement;
private resizeObserver: ResizeObserver;
@internalProperty() private hasFocus = false;
@internalProperty() private hasHelpTextSlot = false;
@internalProperty() private hasLabelSlot = false;
@internalProperty() private hasTooltip = false;
/** The input's name attribute. */
name = '';
@property() name = '';
/** The input's value attribute. */
value: number;
@property({ type: Number }) value: number;
/** The range's label. Alternatively, you can use the label slot. */
label = '';
@property() label = '';
/** The range's help text. Alternatively, you can use the help-text slot. */
helpText = '';
@property({ attribute: 'help-text' }) helpText = '';
/** Disables the input. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/**
* This will be true when the control is in an invalid state. Validity in range inputs is determined by the message
* provided by the `setCustomValidity` method.
*/
invalid = false;
@property({ type: Boolean, reflect: true }) invalid = false;
/** The input's min attribute. */
min = 0;
@property({ type: Number }) min = 0;
/** The input's max attribute. */
max = 100;
@property({ type: Number }) max = 100;
/** The input's step attribute. */
step = 1;
@property({ type: Number }) step = 1;
/** The preferred placedment of the tooltip. */
tooltip: 'top' | 'bottom' | 'none' = 'top';
@property() tooltip: 'top' | 'bottom' | 'none' = 'top';
/** A function used to format the tooltip's value. */
tooltipFormatter = (value: number) => value.toString();
@property() tooltipFormatter = (value: number) => value.toString();
onConnect() {
/** Emitted when the control's value changes. */
@event('sl-change') slChange: EventEmitter<void>;
/** Emitted when the control loses focus. */
@event('sl-blur') slBlur: EventEmitter<void>;
/** Emitted when the control gains focus. */
@event('sl-focus') slFocus: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
this.handleSlotChange = this.handleSlotChange.bind(this);
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
@ -100,12 +93,21 @@ export default class SlRange extends Shoemaker {
this.handleSlotChange();
}
onReady() {
firstUpdated() {
this.syncTooltip();
this.resizeObserver = new ResizeObserver(() => this.syncTooltip());
}
onDisconnect() {
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('label') || changedProps.has('helpText')) {
this.handleSlotChange();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
}
@ -127,7 +129,7 @@ export default class SlRange extends Shoemaker {
handleInput() {
this.value = Number(this.input.value);
this.emit('sl-change');
this.slChange.emit();
requestAnimationFrame(() => this.syncTooltip());
}
@ -135,14 +137,14 @@ export default class SlRange extends Shoemaker {
handleBlur() {
this.hasFocus = false;
this.hasTooltip = false;
this.emit('sl-blur');
this.slBlur.emit();
this.resizeObserver.unobserve(this.input);
}
handleFocus() {
this.hasFocus = true;
this.hasTooltip = true;
this.emit('sl-focus');
this.slFocus.emit();
this.resizeObserver.observe(this.input);
}
@ -167,14 +169,6 @@ export default class SlRange extends Shoemaker {
}
}
watchLabel() {
this.handleSlotChange();
}
watchHelpText() {
this.handleSlotChange();
}
render() {
return renderFormControl(
{
@ -198,29 +192,24 @@ export default class SlRange extends Shoemaker {
'range--tooltip-top': this.tooltip === 'top',
'range--tooltip-bottom': this.tooltip === 'bottom'
})}
ontouchstart=${this.handleTouchStart.bind(this)}
@touchstart=${this.handleTouchStart.bind(this)}
>
<input
part="input"
ref=${(el: HTMLInputElement) => (this.input = el)}
type="range"
class="range__control"
name=${this.name}
disabled=${this.disabled ? true : null}
min=${this.min}
max=${this.max}
step=${this.step}
.value=${this.value}
oninput=${this.handleInput.bind(this)}
onfocus=${this.handleFocus.bind(this)}
onblur=${this.handleBlur.bind(this)}
?disabled=${this.disabled ? true : null}
min=${ifDefined(this.min)}
max=${ifDefined(this.max)}
step=${ifDefined(this.step)}
@input=${this.handleInput.bind(this)}
@focus=${this.handleFocus.bind(this)}
@blur=${this.handleBlur.bind(this)}
/>
${this.tooltip !== 'none'
? html`
<output part="tooltip" ref=${(el: HTMLOutputElement) => (this.output = el)} class="range__tooltip">
${this.tooltipFormatter(this.value)}
</output>
`
? html` <output part="tooltip" class="range__tooltip"> ${this.tooltipFormatter(this.value)} </output> `
: ''}
</div>
`

Wyświetl plik

@ -1,4 +1,8 @@
import { classMap, html, styleMap, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { styleMap } from 'lit-html/directives/style-map';
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./rating.scss';
import { focusVisible } from '../../internal/focus-visible';
import { clamp } from '../../internal/math';
@ -10,36 +14,37 @@ import { clamp } from '../../internal/math';
* @dependency sl-icon
*
* @part base - The component's base wrapper.
*
* @emit sl-change - Emitted when the rating's value changes.
*/
export default class SlRating extends Shoemaker {
static tag = 'sl-rating';
static props = ['hoverValue', 'isHovering', 'value', 'max', 'precision', 'readonly', 'disabled', 'symbol'];
static reflect = ['readonly', 'disabled'];
static styles = styles;
@customElement('sl-rating')
export class SlRating extends LitElement {
static styles = unsafeCSS(styles);
private hoverValue = 0;
private isHovering = false;
private rating: HTMLElement;
@query('.rating') rating: HTMLElement;
@internalProperty() private hoverValue = 0;
@internalProperty() private isHovering = false;
/** The current rating. */
value = 0;
@property({ type: Number }) value = 0;
/** The highest rating to show. */
max = 5;
@property({ type: Number }) max = 5;
/** The minimum increment value allowed by the control. */
precision = 1;
@property({ type: Number }) precision = 1;
/** Makes the rating readonly. */
readonly = false;
@property({ type: Boolean, reflect: true }) readonly = false;
/** Disables the rating. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** The name of the icon to display as the symbol. */
symbol: string | ((value: number) => string) = 'star-fill';
// @ts-ignore
@property() getSymbol = (value?: number) => '<sl-icon name="star-fill"></sl-icon>';
/** Emitted when the rating's value changes. */
@event('sl-change') slChange: EventEmitter<void>;
/** Sets focus on the rating. */
setFocus(options?: FocusOptions) {
@ -51,11 +56,20 @@ export default class SlRating extends Shoemaker {
this.rating.blur();
}
onReady() {
firstUpdated() {
focusVisible.observe(this.rating);
}
onDisconnect() {
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('value')) {
this.slChange.emit();
}
}
disconnectedCallback() {
super.disconnectedCallback();
focusVisible.unobserve(this.rating);
}
@ -125,10 +139,6 @@ export default class SlRating extends Shoemaker {
return Math.ceil(numberToRound * multiplier) / multiplier;
}
watchValue() {
this.emit('sl-change');
}
render() {
const counter = Array.from(Array(this.max).keys());
let displayValue = 0;
@ -141,7 +151,6 @@ export default class SlRating extends Shoemaker {
return html`
<div
ref=${(el: HTMLElement) => (this.rating = el)}
part="base"
class=${classMap({
rating: true,
@ -154,18 +163,17 @@ export default class SlRating extends Shoemaker {
aria-valuemin=${0}
aria-valuemax=${this.max}
tabindex=${this.disabled ? '-1' : '0'}
onclick=${this.handleClick.bind(this)}
onkeydown=${this.handleKeyDown.bind(this)}
onmouseenter=${this.handleMouseEnter.bind(this)}
onmouseleave=${this.handleMouseLeave.bind(this)}
onmousemove=${this.handleMouseMove.bind(this)}
@click=${this.handleClick}
@keydown=${this.handleKeyDown}
@mouseenter=${this.handleMouseEnter}
@mouseleave=${this.handleMouseLeave}
@mousemove=${this.handleMouseMove}
>
<span class="rating__symbols rating__symbols--inactive">
${counter.map(index => {
// Users can click the current value to clear the rating. When this happens, we set this.isHovering to
// false to prevent the hover state from confusing them as they move the mouse out of the control. This
// extra mouseenter will reinstate it if they happen to mouse over an adjacent symbol.
const symbol = typeof this.symbol === 'function' ? this.symbol(index + 1) : this.symbol;
return html`
<span
class=${classMap({
@ -173,9 +181,9 @@ export default class SlRating extends Shoemaker {
'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
})}
role="presentation"
onmouseenter=${this.handleMouseEnter.bind(this)}
@mouseenter=${this.handleMouseEnter.bind(this)}
>
<sl-icon .name=${symbol}></sl-icon>
${unsafeHTML(this.getSymbol(index + 1))}
</span>
`;
})}
@ -183,7 +191,6 @@ export default class SlRating extends Shoemaker {
<span class="rating__symbols rating__symbols--indicator">
${counter.map(index => {
const symbol = typeof this.symbol === 'function' ? this.symbol(index + 1) : this.symbol;
return html`
<span
class=${classMap({
@ -192,11 +199,11 @@ export default class SlRating extends Shoemaker {
})}
style=${styleMap({
clipPath:
displayValue > index + 1 ? null : `inset(0 ${100 - ((displayValue - index) / 1) * 100}% 0 0)`
displayValue > index + 1 ? 'none' : `inset(0 ${100 - ((displayValue - index) / 1) * 100}% 0 0)`
})}
role="presentation"
>
<sl-icon .name=${symbol}></sl-icon>
${unsafeHTML(this.getSymbol(index + 1))}
</span>
`;
})}

Wyświetl plik

@ -1,41 +1,49 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property } from 'lit-element';
/**
* @since 2.0
* @status stable
*/
export default class SlRelativeTime extends Shoemaker {
static tag = 'sl-relative-time';
static props = ['isoTime', 'relativeTime', 'titleTime', 'date', 'locale', 'format', 'numeric', 'sync'];
private isoTime = '';
private relativeTime = '';
private titleTime = '';
@customElement('sl-relative-time')
export class SlRelativeTime extends LitElement {
private updateTimeout: any;
@internalProperty() private isoTime = '';
@internalProperty() private relativeTime = '';
@internalProperty() private titleTime = '';
/** The date from which to calculate time from. */
date: Date | string;
@property() date: Date | string;
/** The locale to use when formatting the number. */
locale: string;
@property() locale: string;
/** The formatting style to use. */
format: 'long' | 'short' | 'narrow' = 'long';
@property() format: 'long' | 'short' | 'narrow' = 'long';
/**
* When `auto`, values such as "yesterday" and "tomorrow" will be shown when possible. When `always`, values such as
* "1 day ago" and "in 1 day" will be shown.
*/
numeric: 'always' | 'auto' = 'auto';
@property() numeric: 'always' | 'auto' = 'auto';
/** Keep the displayed value up to date as time passes. */
sync = false;
@property({ type: Boolean }) sync = false;
onReady() {
firstUpdated() {
this.updateTime();
}
onDisconnect() {
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (['date', 'locale', 'format', 'numeric', 'sync'].find(prop => changedProps.has(prop))) {
this.updateTime();
}
}
disconnectedCallback() {
super.disconnectedCallback();
clearTimeout(this.updateTimeout);
}
@ -106,26 +114,6 @@ export default class SlRelativeTime extends Shoemaker {
}
}
watchDate() {
this.updateTime();
}
watchLocale() {
this.updateTime();
}
watchFormat() {
this.updateTime();
}
watchNumeric() {
this.updateTime();
}
watchSync() {
this.updateTime();
}
render() {
return html` <time datetime=${this.isoTime} title=${this.titleTime}>${this.relativeTime}</time> `;
}

Wyświetl plik

@ -1,25 +1,27 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, unsafeCSS } from 'lit-element';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./resize-observer.scss';
/**
* @since 2.0
* @status experimental
*
* @emit sl-resize - Emitted when the element is resized. Event details will contain:
* `{ entries: ResizeObserverEntry[] }`
*/
export default class SlResizeObserver extends Shoemaker {
static tag = 'sl-resize-observer';
static styles = styles;
@customElement('sl-resize-observer')
export class SlResizeObserver extends LitElement {
static styles = unsafeCSS(styles);
private resizeObserver: ResizeObserver;
private observedElements: HTMLElement[] = [];
onReady() {
this.resizeObserver = new ResizeObserver(entries => this.emit('sl-resize', { detail: { entries } }));
@event('sl-resize') slResize: EventEmitter<{ entries: ResizeObserverEntry[] }>;
connectedCallback() {
super.connectedCallback();
this.resizeObserver = new ResizeObserver(entries => this.slResize.emit({ detail: { entries } }));
}
onDisconnect() {
disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver.disconnect();
}
@ -39,6 +41,6 @@ export default class SlResizeObserver extends Shoemaker {
}
render() {
return html` <slot onslotchange=${this.handleSlotChange.bind(this)} /> `;
return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
}
}

Wyświetl plik

@ -1,4 +1,4 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import styles from 'sass:./responsive-embed.scss';
/**
@ -7,18 +7,25 @@ import styles from 'sass:./responsive-embed.scss';
*
* @part base - The component's base wrapper.
*/
export default class SlResponsiveEmbed extends Shoemaker {
static tag = 'sl-responsive-embed';
static props = ['aspectRatio'];
static styles = styles;
@customElement('sl-responsive-embed')
export class SlResponsiveEmbed extends LitElement {
static styles = unsafeCSS(styles);
private base: HTMLElement;
@query('.responsive-embed') base: HTMLElement;
/**
* The aspect ratio of the embedded media in the format of `width:height`, e.g. `16:9`, `4:3`, or `1:1`. Ratios not in
* this format will be ignored.
*/
aspectRatio = '16:9';
@property({ attribute: 'aspect-ratio' }) aspectRatio = '16:9';
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('aspectRatio')) {
this.updateAspectRatio();
}
}
updateAspectRatio() {
const split = this.aspectRatio.split(':');
@ -28,14 +35,10 @@ export default class SlResponsiveEmbed extends Shoemaker {
this.base.style.paddingBottom = x && y ? `${(y / x) * 100}%` : '';
}
watchAspectRatio() {
this.updateAspectRatio();
}
render() {
return html`
<div ref=${(el: HTMLElement) => (this.base = el)} part="base" class="responsive-embed">
<slot onslotchange=${() => this.updateAspectRatio()} />
<div part="base" class="responsive-embed">
<slot @slotchange=${() => this.updateAspectRatio()}></slot>
</div>
`;
}

Wyświetl plik

@ -1,4 +1,15 @@
import { classMap, html, Hole, Shoemaker } from '@shoelace-style/shoemaker';
import {
LitElement,
TemplateResult,
customElement,
html,
internalProperty,
property,
query,
unsafeCSS
} from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./select.scss';
import { SlDropdown, SlIconButton, SlMenu, SlMenuItem } from '../../shoelace';
import { renderFormControl } from '../../internal/form-control';
@ -30,115 +41,124 @@ let id = 0;
* @part menu - The select menu, a <sl-menu> element.
* @part tag - The multiselect option, a <sl-tag> element.
* @part tags - The container in which multiselect options are rendered.
*
* @emit sl-change - Emitted when the control's value changes.
* @emit sl-focus - Emitted when the control gains focus.
* @emit sl-blur - Emitted when the control loses focus.
*/
export default class SlSelect extends Shoemaker {
static tag = 'sl-select';
static props = [
'hasFocus',
'hasHelpTextSlot',
'hasLabelSlot',
'isOpen',
'items',
'displayLabel',
'displayTags',
'multiple',
'maxTagsVisible',
'disabled',
'name',
'placeholder',
'size',
'hoist',
'value',
'pill',
'label',
'helpText',
'required',
'clearable',
'invalid'
];
static reflect = ['disabled', 'invalid'];
static styles = styles;
@customElement('sl-select')
export class SlSelect extends LitElement {
static styles = unsafeCSS(styles);
@query('.select') dropdown: SlDropdown;
@query('.select__hidden-select') input: HTMLInputElement;
@query('.select__menu') menu: SlMenu;
private displayLabel = '';
private displayTags: Hole[] = [];
private dropdown: SlDropdown;
private hasFocus = false;
private hasHelpTextSlot = false;
private hasLabelSlot = false;
private helpTextId = `select-help-text-${id}`;
private input: HTMLInputElement;
private inputId = `select-${++id}`;
private isOpen = false;
private labelId = `select-label-${id}`;
private menu: SlMenu;
private resizeObserver: ResizeObserver;
@internalProperty() private hasFocus = false;
@internalProperty() private hasHelpTextSlot = false;
@internalProperty() private hasLabelSlot = false;
@internalProperty() private isOpen = false;
@internalProperty() private displayLabel = '';
@internalProperty() private displayTags: TemplateResult[] = [];
/** Enables multiselect. With this enabled, value will be an array. */
multiple = false;
@property({ type: Boolean, reflect: true }) multiple = false;
/**
* The maximum number of tags to show when `multiple` is true. After the maximum, "+n" will be shown to indicate the
* number of additional items that are selected. Set to -1 to remove the limit.
*/
maxTagsVisible = 3;
@property({ type: Number }) maxTagsVisible = 3;
/** Disables the select control. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** The select's name. */
name = '';
@property() name = '';
/** The select's placeholder text. */
placeholder = '';
@property() placeholder = '';
/** The select's size. */
size: 'small' | 'medium' | 'large' = 'medium';
@property() size: 'small' | 'medium' | 'large' = 'medium';
/**
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
* `overflow: auto|scroll`.
*/
hoist = false;
@property({ type: Boolean, reflect: true }) hoist = false;
/** The value of the control. This will be a string or an array depending on `multiple`. */
value: string | Array<string> = '';
@property() value: string | Array<string> = '';
/** Draws a pill-style select with rounded edges. */
pill = false;
@property({ type: Boolean, reflect: true }) pill = false;
/** The select's label. Alternatively, you can use the label slot. */
label = '';
@property() label: string;
/** The select's help text. Alternatively, you can use the help-text slot. */
helpText = '';
@property({ attribute: 'help-text' }) helpText: string;
/** The select's required attribute. */
required = false;
@property({ type: Boolean, reflect: true }) required = false;
/** Adds a clear button when the select is populated. */
clearable = false;
@property({ type: Boolean, reflect: true }) clearable = false;
/** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */
invalid = false;
@property({ type: Boolean, reflect: true }) invalid = false;
onConnect() {
/** Emitted when the control's value changes. */
@event('sl-change') slChange: EventEmitter<void>;
/** Emitted when the control gains focus. */
@event('sl-focus') slFocus: EventEmitter<void>;
/** Emitted when the control loses focus. */
@event('sl-blur') slBlur: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
this.handleSlotChange = this.handleSlotChange.bind(this);
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
this.handleSlotChange();
}
onReady() {
firstUpdated() {
this.resizeObserver = new ResizeObserver(() => this.resizeMenu());
// We need to do an initial sync after the component has rendered, so this will suppress the re-render warning
requestAnimationFrame(() => this.syncItemsFromValue());
}
onDisconnect() {
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('help-text') || changedProps.has('label')) {
this.handleSlotChange();
}
if (changedProps.has('multiple')) {
// Cast to array | string based on `this.multiple`
const value = this.getValueAsArray();
this.value = this.multiple ? value : value[0] || '';
this.syncItemsFromValue();
}
if (changedProps.has('disabled')) {
if (this.disabled && this.isOpen) {
this.dropdown.hide();
}
}
if (changedProps.has('value')) {
this.syncItemsFromValue();
this.slChange.emit();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
}
@ -168,12 +188,12 @@ export default class SlSelect extends Shoemaker {
handleBlur() {
this.hasFocus = false;
this.emit('sl-blur');
this.slBlur.emit();
}
handleFocus() {
this.hasFocus = true;
this.emit('sl-focus');
this.slFocus.emit();
}
handleClearClick(event: MouseEvent) {
@ -291,7 +311,10 @@ export default class SlSelect extends Shoemaker {
resizeMenu() {
const box = this.shadowRoot?.querySelector('.select__box') as HTMLElement;
this.menu.style.width = `${box.clientWidth}px`;
this.dropdown.reposition();
if (this.dropdown) {
this.dropdown.reposition();
}
}
syncItemsFromValue() {
@ -314,9 +337,9 @@ export default class SlSelect extends Shoemaker {
size=${this.size}
pill=${this.pill}
clearable
onclick=${this.handleTagInteraction.bind(this)}
onkeydown=${this.handleTagInteraction.bind(this)}
onsl-clear=${(event: CustomEvent) => {
@click=${this.handleTagInteraction}
@keydown=${this.handleTagInteraction}
@sl-clear=${(event: CustomEvent) => {
event.stopPropagation();
if (!this.disabled) {
item.checked = false;
@ -356,32 +379,6 @@ export default class SlSelect extends Shoemaker {
}
}
watchDisabled() {
if (this.disabled && this.isOpen) {
this.dropdown.hide();
}
}
watchHelpText() {
this.handleSlotChange();
}
watchLabel() {
this.handleSlotChange();
}
watchMultiple() {
// Cast to array | string based on `this.multiple`
const value = this.getValueAsArray();
this.value = this.multiple ? value : value[0] || '';
this.syncItemsFromValue();
}
watchValue() {
this.syncItemsFromValue();
this.emit('sl-change');
}
render() {
const hasSelection = this.multiple ? this.value.length > 0 : this.value !== '';
@ -400,7 +397,6 @@ export default class SlSelect extends Shoemaker {
html`
<sl-dropdown
part="base"
ref=${(el: SlDropdown) => (this.dropdown = el)}
.hoist=${this.hoist}
.closeOnSelect=${!this.multiple}
.containingElement=${this}
@ -412,7 +408,7 @@ export default class SlSelect extends Shoemaker {
'select--clearable': this.clearable,
'select--disabled': this.disabled,
'select--multiple': this.multiple,
'select--has-tags': this.multiple && hasSelection,
'select--has-tags': this.multiple && this.displayTags.length > 0,
'select--placeholder-visible': this.displayLabel === '',
'select--small': this.size === 'small',
'select--medium': this.size === 'medium',
@ -420,8 +416,8 @@ export default class SlSelect extends Shoemaker {
'select--pill': this.pill,
'select--invalid': this.invalid
})}
onsl-show=${this.handleMenuShow.bind(this)}
onsl-hide=${this.handleMenuHide.bind(this)}
@sl-show=${this.handleMenuShow}
@sl-hide=${this.handleMenuHide}
>
<div
slot="trigger"
@ -433,9 +429,9 @@ export default class SlSelect extends Shoemaker {
aria-haspopup="true"
aria-expanded=${this.isOpen ? 'true' : 'false'}
tabindex=${this.disabled ? '-1' : '0'}
onblur=${this.handleBlur.bind(this)}
onfocus=${this.handleFocus.bind(this)}
onkeydown=${this.handleKeyDown.bind(this)}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@keydown=${this.handleKeyDown}
>
<div class="select__label">
${this.displayTags.length
@ -449,20 +445,19 @@ export default class SlSelect extends Shoemaker {
exportparts="base:clear-button"
class="select__clear"
name="x-circle"
onclick=${this.handleClearClick.bind(this)}
@click=${this.handleClearClick}
tabindex="-1"
/>
></sl-icon-button>
`
: ''}
<span part="icon" class="select__icon">
<sl-icon name="chevron-down" />
<sl-icon name="chevron-down"></sl-icon>
</span>
<!-- The hidden input tricks the browser's built-in validation so it works as expected. We use an input
<!-- The hidden input tricks the browser's built-in validation so it works as expected. We use an input
instead of a select because, otherwise, iOS will show a list of options during validation. -->
<input
ref=${(el: HTMLInputElement) => (this.input = el)}
class="select__hidden-select"
aria-hidden="true"
required=${this.required ? true : null}
@ -471,13 +466,8 @@ export default class SlSelect extends Shoemaker {
/>
</div>
<sl-menu
ref=${(el: SlMenu) => (this.menu = el)}
part="menu"
class="select__menu"
onsl-select=${this.handleMenuSelect.bind(this)}
>
<slot onslotchange=${this.handleSlotChange} />
<sl-menu part="menu" class="select__menu" @sl-select=${this.handleMenuSelect}>
<slot @slotchange=${this.handleSlotChange}></slot>
</sl-menu>
</sl-dropdown>
`

Wyświetl plik

@ -1,4 +1,5 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import styles from 'sass:./skeleton.scss';
/**
@ -8,13 +9,12 @@ import styles from 'sass:./skeleton.scss';
* @part base - The component's base wrapper.
* @part indicator - The skeleton's indicator which is responsible for its color and animation.
*/
export default class SlSkeleton extends Shoemaker {
static tag = 'sl-skeleton';
static props = ['effect'];
static styles = styles;
@customElement('sl-skeleton')
export class SlSkeleton extends LitElement {
static styles = unsafeCSS(styles);
/** Determines which effect the skeleton will use. */
effect: 'pulse' | 'sheen' | 'none' = 'sheen';
@property() effect: 'pulse' | 'sheen' | 'none' = 'sheen';
render() {
return html`
@ -28,7 +28,7 @@ export default class SlSkeleton extends Shoemaker {
aria-busy="true"
aria-live="polite"
>
<div part="indicator" class="skeleton__indicator" />
<div part="indicator" class="skeleton__indicator"></div>
</div>
`;
}

Wyświetl plik

@ -1,4 +1,4 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, unsafeCSS } from 'lit-element';
import styles from 'sass:./spinner.scss';
/**
@ -7,9 +7,9 @@ import styles from 'sass:./spinner.scss';
*
* @part base - The component's base wrapper.
*/
export default class SlSpinner extends Shoemaker {
static tag = 'sl-spinner';
static styles = styles;
@customElement('sl-spinner')
export class SlSpinner extends LitElement {
static styles = unsafeCSS(styles);
render() {
return html` <span part="base" class="spinner" aria-busy="true" aria-live="polite"></span> `;

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./switch.scss';
let id = 0;
@ -13,39 +15,44 @@ let id = 0;
* @part control - The switch control.
* @part thumb - The switch position indicator.
* @part label - The switch label.
*
* @emit sl-blur - Emitted when the control loses focus.
* @emit sl-change - Emitted when the control's checked state changes.
* @emit sl-focus - Emitted when the control gains focus.
*/
export default class SlSwitch extends Shoemaker {
static tag = 'sl-switch';
static props = ['hasFocus', 'name', 'value', 'disabled', 'required', 'checked', 'invalid'];
static reflect = ['disabled', 'checked', 'invalid'];
static styles = styles;
@customElement('sl-switch')
export class SlSwitch extends LitElement {
static styles = unsafeCSS(styles);
@query('input[type="checkbox"]') input: HTMLInputElement;
private switchId = `switch-${++id}`;
private labelId = `switch-label-${id}`;
private input: HTMLInputElement;
private hasFocus = false;
@internalProperty() private hasFocus = false;
/** The switch's name attribute. */
name: string;
@property({ reflect: true }) name: string;
/** The switch's value attribute. */
value: string;
@property() value: string;
/** Disables the switch. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** Makes the switch a required field. */
required = false;
@property({ type: Boolean, reflect: true }) required = false;
/** Draws the switch in a checked state. */
checked = false;
@property({ type: Boolean, reflect: true }) checked = false;
/** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */
invalid = false;
@property({ type: Boolean, reflect: true }) invalid = false;
/** Emitted when the control loses focus. */
@event('sl-blur') slBlur: EventEmitter<void>;
/** Emitted when the control's checked state changes. */
@event('sl-change') slChange: EventEmitter<void>;
/** Emitted when the control gains focus. */
@event('sl-focus') slFocus: EventEmitter<void>;
/** Sets focus on the switch. */
setFocus(options?: FocusOptions) {
@ -69,17 +76,17 @@ export default class SlSwitch extends Shoemaker {
}
handleClick() {
this.checked = this.input.checked;
this.checked = !this.checked;
}
handleBlur() {
this.hasFocus = false;
this.emit('sl-blur');
this.slBlur.emit();
}
handleFocus() {
this.hasFocus = true;
this.emit('sl-focus');
this.slFocus.emit();
}
handleKeyDown(event: KeyboardEvent) {
@ -100,9 +107,11 @@ export default class SlSwitch extends Shoemaker {
this.input.focus();
}
watchChecked() {
this.input.checked = this.checked;
this.emit('sl-change');
checkedChanged() {
if (this.input) {
this.input.checked = this.checked;
this.slChange.emit();
}
}
render() {
@ -116,32 +125,31 @@ export default class SlSwitch extends Shoemaker {
'switch--disabled': this.disabled,
'switch--focused': this.hasFocus
})}
onmousedown=${this.handleMouseDown.bind(this)}
@mousedown=${this.handleMouseDown}
>
<span part="control" class="switch__control">
<span part="thumb" class="switch__thumb" />
<span part="thumb" class="switch__thumb"></span>
<input
ref=${(el: HTMLInputElement) => (this.input = el)}
id=${this.switchId}
type="checkbox"
name=${this.name}
.value=${this.value}
checked=${this.checked ? true : null}
disabled=${this.disabled ? true : null}
required=${this.required ? true : null}
value=${this.value}
?checked=${this.checked}
?disabled=${this.disabled}
?required=${this.required}
role="switch"
aria-checked=${this.checked ? 'true' : 'false'}
aria-labelledby=${this.labelId}
onclick=${this.handleClick.bind(this)}
onblur=${this.handleBlur.bind(this)}
onfocus=${this.handleFocus.bind(this)}
onkeydown=${this.handleKeyDown.bind(this)}
@click=${this.handleClick}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@keydown=${this.handleKeyDown}
/>
</span>
<span part="label" id=${this.labelId} class="switch__label">
<slot />
<slot></slot>
</span>
</label>
`;

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./tab-group.scss';
import { SlTab, SlTabPanel } from '../../shoelace';
import { getOffset } from '../../internal/offset';
@ -20,33 +22,37 @@ import { focusVisible } from '../../internal/focus-visible';
* @part active-tab-indicator - An element that displays the currently selected tab. This is a child of the tabs container.
* @part body - The tab group body where tab panels are slotted in.
* @part scroll-button - The previous and next scroll buttons that appear when tabs are scrollable.
*
* @emit sl-tab-show - Emitted when a tab is shown. Event details will contain: `{ tab: string }`
* @emit sl-tab-hide - Emitted when a tab is hidden. Event details will contain: `{ tab: string }`
*/
export default class SlTabGroup extends Shoemaker {
static tag = 'sl-tab-group';
static props = ['hasScrollControls', 'placement', 'noScrollControls'];
static styles = styles;
@customElement('sl-tab-group')
export class SlTabGroup extends LitElement {
static styles = unsafeCSS(styles);
@query('.tab-group') tabGroup: HTMLElement;
@query('.tab-group__body') body: HTMLElement;
@query('.tab-group__nav') nav: HTMLElement;
@query('.tab-group__active-tab-indicator') activeTabIndicator: HTMLElement;
private activeTab: SlTab;
private activeTabIndicator: HTMLElement;
private body: HTMLElement;
private hasScrollControls = false;
private mutationObserver: MutationObserver;
private nav: HTMLElement;
private panels: SlTabPanel[] = [];
private resizeObserver: ResizeObserver;
private tabGroup: HTMLElement;
private tabs: SlTab[] = [];
private panels: SlTabPanel[] = [];
@internalProperty() private hasScrollControls = false;
/** The placement of the tabs. */
placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
@property() placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
/** Disables the scroll arrows that appear when tabs overflow. */
noScrollControls = false;
@property({ attribute: 'no-scroll-controls', type: Boolean, reflect: true }) noScrollControls = false;
onReady() {
/** Emitted when a tab is shown. */
@event('sl-tab-show') slTabShow: EventEmitter<{ tab: string }>;
/** Emitted when a tab is hidden. */
@event('sl-tab-hide') slTabHide: EventEmitter<{ tab: string }>;
firstUpdated() {
this.syncTabsAndPanels();
// Set initial tab state when the tabs first become visible
@ -78,7 +84,19 @@ export default class SlTabGroup extends Shoemaker {
this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });
}
onDisconnect() {
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('placement')) {
this.syncActiveTabIndicator();
}
if (changedProps.has('noScrollControls')) {
this.updateScrollControls();
}
}
disconnectedCallback() {
this.mutationObserver.disconnect();
focusVisible.unobserve(this.tabGroup);
this.resizeObserver.unobserve(this.nav);
@ -215,10 +233,10 @@ export default class SlTabGroup extends Shoemaker {
// Emit events
if (emitEvents) {
if (previousTab) {
this.emit('sl-tab-hide', { detail: { name: previousTab.panel } });
this.slTabHide.emit({ detail: { name: previousTab.panel } });
}
this.emit('sl-tab-show', { detail: { name: this.activeTab.panel } });
this.slTabShow.emit({ detail: { name: this.activeTab.panel } });
}
}
}
@ -273,19 +291,10 @@ export default class SlTabGroup extends Shoemaker {
this.syncActiveTabIndicator();
}
watchPlacement() {
this.syncActiveTabIndicator();
}
watchNoScrollControls() {
this.updateScrollControls();
}
render() {
return html`
<div
part="base"
ref=${(el: HTMLElement) => (this.tabGroup = el)}
class=${classMap({
'tab-group': true,
'tab-group--top': this.placement === 'top',
@ -294,8 +303,8 @@ export default class SlTabGroup extends Shoemaker {
'tab-group--right': this.placement === 'right',
'tab-group--has-scroll-controls': this.hasScrollControls
})}
onclick=${this.handleClick.bind(this)}
onkeydown=${this.handleKeyDown.bind(this)}
@click=${this.handleClick}
@keydown=${this.handleKeyDown}
>
<div class="tab-group__nav-container">
${this.hasScrollControls
@ -304,19 +313,15 @@ export default class SlTabGroup extends Shoemaker {
class="tab-group__scroll-button tab-group__scroll-button--left"
exportparts="base:scroll-button"
name="chevron-left"
onclick=${this.handleScrollLeft.bind(this)}
/>
@click=${this.handleScrollLeft}
></sl-icon-button>
`
: ''}
<div ref=${(el: HTMLElement) => (this.nav = el)} part="nav" class="tab-group__nav">
<div part="nav" class="tab-group__nav">
<div part="tabs" class="tab-group__tabs" role="tablist">
<div
ref=${(el: HTMLElement) => (this.activeTabIndicator = el)}
part="active-tab-indicator"
class="tab-group__active-tab-indicator"
/>
<slot name="nav" onslotchange=${this.syncTabsAndPanels.bind(this)} />
<div part="active-tab-indicator" class="tab-group__active-tab-indicator"></div>
<slot name="nav" @slotchange=${this.syncTabsAndPanels}></slot>
</div>
</div>
@ -326,14 +331,14 @@ export default class SlTabGroup extends Shoemaker {
class="tab-group__scroll-button tab-group__scroll-button--right"
exportparts="base:scroll-button"
name="chevron-right"
onclick=${this.handleScrollRight.bind(this)}
/>
@click=${this.handleScrollRight}
></sl-icon-button>
`
: ''}
</div>
<div ref=${(el: HTMLElement) => (this.body = el)} part="body" class="tab-group__body">
<slot onslotchange=${this.syncTabsAndPanels.bind(this)} />
<div part="body" class="tab-group__body">
<slot @slotchange=${this.syncTabsAndPanels}></slot>
</div>
</div>
`;

Wyświetl plik

@ -1,4 +1,4 @@
import { html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, unsafeCSS } from 'lit-element';
import styles from 'sass:./tab-panel.scss';
let id = 0;
@ -11,21 +11,19 @@ let id = 0;
*
* @part base - The component's base wrapper.
*/
export default class SlTabPanel extends Shoemaker {
static tag = 'sl-tab-panel';
static props = ['name', 'active'];
static reflect = ['name', 'active'];
static styles = styles;
@customElement('sl-tab-panel')
export class SlTabPanel extends LitElement {
static styles = unsafeCSS(styles);
private componentId = `tab-panel-${++id}`;
/** The tab panel's name. */
name = '';
@property() name = '';
/** When true, the tab panel will be shown. */
active = false;
@property({ type: Boolean, reflect: true }) active = false;
onReady() {
firstUpdated() {
this.id = this.id || this.componentId;
}
@ -40,7 +38,7 @@ export default class SlTabPanel extends Shoemaker {
aria-selected=${this.active ? 'true' : 'false'}
aria-hidden=${this.active ? 'false' : 'true'}
>
<slot />
<slot></slot>
</div>
`;
}

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./tab.scss';
let id = 0;
@ -13,29 +15,29 @@ let id = 0;
*
* @part base - The component's base wrapper.
* @part close-button - The close button, which is the icon button's base wrapper.
*
* @emit sl-close - Emitted when the tab is closable and the close button is activated.
*/
export default class SlTab extends Shoemaker {
static tag = 'sl-tab';
static props = ['panel', 'active', 'closable', 'disabled'];
static reflect = ['panel', 'active', 'closable', 'disabled'];
static styles = styles;
@customElement('sl-tab')
export class SlTab extends LitElement {
static styles = unsafeCSS(styles);
@query('.tab') tab: HTMLElement;
private componentId = `tab-${++id}`;
private tab: HTMLElement;
/** The name of the tab panel the tab will control. The panel must be located in the same tab group. */
panel = '';
@property() panel = '';
/** Draws the tab in an active state. */
active = false;
@property({ type: Boolean, reflect: true }) active = false;
/** Makes the tab closable and shows a close icon. */
closable = false;
@property({ type: Boolean, reflect: true }) closable = false;
/** Draws the tab in a disabled state. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** Emitted when the tab is closable and the close button is activated. */
@event('sl-close') slClose: EventEmitter<void>;
/** Sets focus to the tab. */
setFocus(options?: FocusOptions) {
@ -48,7 +50,7 @@ export default class SlTab extends Shoemaker {
}
handleCloseClick() {
this.emit('sl-close');
this.slClose.emit();
}
render() {
@ -58,7 +60,6 @@ export default class SlTab extends Shoemaker {
return html`
<div
part="base"
ref=${(el: HTMLElement) => (this.tab = el)}
class=${classMap({
tab: true,
'tab--active': this.active,
@ -70,17 +71,17 @@ export default class SlTab extends Shoemaker {
aria-selected=${this.active ? 'true' : 'false'}
tabindex=${this.disabled || !this.active ? '-1' : '0'}
>
<slot />
<slot></slot>
${this.closable
? html`
<sl-icon-button
name="x"
exportparts="base:close-button"
class="tab__close-button"
onclick=${this.handleCloseClick.bind(this)}
@click=${this.handleCloseClick}
tabindex="-1"
aria-hidden="true"
/>
></sl-icon-button>
`
: ''}
</div>

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./tag.scss';
/**
@ -12,29 +14,28 @@ import styles from 'sass:./tag.scss';
* @part base - The component's base wrapper.
* @part content - The tag content.
* @part clear-button - The clear button.
*
* @emit sl-clear - Emitted when the clear button is activated.
*/
export default class SlTag extends Shoemaker {
static tag = 'sl-tag';
static props = ['type', 'size', 'pill', 'clearable'];
static reflect = ['type', 'size', 'pill', 'clearable'];
static styles = styles;
@customElement('sl-tag')
export class SlTag extends LitElement {
static styles = unsafeCSS(styles);
/** The tag's type. */
type: 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'text' = 'primary';
@property({ reflect: true }) type: 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'text' = 'primary';
/** The tag's size. */
size: 'small' | 'medium' | 'large' = 'medium';
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
/** Draws a pill-style tag with rounded edges. */
pill = false;
@property({ type: Boolean, reflect: true }) pill = false;
/** Makes the tag clearable. */
clearable = false;
@property({ type: Boolean, reflect: true }) clearable = false;
/** Emitted when the clear button is activated. */
@event('sl-clear') slClear: EventEmitter<void>;
handleClearClick() {
this.emit('sl-clear');
this.slClear.emit();
}
render() {
@ -63,7 +64,7 @@ export default class SlTag extends Shoemaker {
})}
>
<span part="content" class="tag__content">
<slot />
<slot></slot>
</span>
${this.clearable
@ -72,8 +73,8 @@ export default class SlTag extends Shoemaker {
exportparts="base:clear-button"
name="x"
class="tag__clear"
onclick=${this.handleClearClick.bind(this)}
/>
@click=${this.handleClearClick}
></sl-icon-button>
`
: ''}
</span>

Wyświetl plik

@ -1,4 +1,7 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./textarea.scss';
import { renderFormControl } from '../../internal/form-control';
import { hasSlot } from '../../internal/slot';
@ -17,131 +20,132 @@ let id = 0;
* @part label - The textarea label.
* @part textarea - The textarea control.
* @part help-text - The textarea help text.
*
* @emit sl-change - Emitted when the control's value changes.
* @emit sl-input - Emitted when the control receives input.
* @emit sl-focus - Emitted when the control gains focus.
* @emit sl-blur - Emitted when the control loses focus.
*/
export default class SlTextarea extends Shoemaker {
static tag = 'sl-textarea';
static props = [
'hasFocus',
'hasHelpTextSlot',
'hasLabelSlot',
'size',
'name',
'value',
'label',
'helpText',
'placeholder',
'rows',
'resize',
'disabled',
'readonly',
'minlength',
'maxlength',
'required',
'invalid',
'autocapitalize',
'autocorrect',
'autocomplete',
'autofocus',
'spellcheck',
'inputmode'
];
static reflect = ['size', 'pill', 'disabled', 'readonly', 'invalid'];
static styles = styles;
@customElement('sl-textarea')
export class SlTextarea extends LitElement {
static styles = unsafeCSS(styles);
@query('.textarea__control') input: HTMLTextAreaElement;
private hasFocus = false;
private hasHelpTextSlot = false;
private hasLabelSlot = false;
private helpTextId = `textarea-help-text-${id}`;
private input: HTMLTextAreaElement;
private inputId = `textarea-${++id}`;
private labelId = `textarea-label-${id}`;
private resizeObserver: ResizeObserver;
@internalProperty() private hasFocus = false;
@internalProperty() private hasHelpTextSlot = false;
@internalProperty() private hasLabelSlot = false;
/** The textarea's size. */
size: 'small' | 'medium' | 'large' = 'medium';
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
/** The textarea's name attribute. */
name = '';
@property() name: string;
/** The textarea's value attribute. */
value = '';
@property() value = '';
/** The textarea's label. Alternatively, you can use the label slot. */
label = '';
@property() label: string;
/** The textarea's help text. Alternatively, you can use the help-text slot. */
helpText = '';
@property({ attribute: 'help-text' }) helpText: string;
/** The textarea's placeholder text. */
placeholder = '';
@property() placeholder = '';
/** The number of rows to display by default. */
rows = 4;
@property({ type: Number }) rows = 4;
/** Controls how the textarea can be resized. */
resize: 'none' | 'vertical' | 'auto' = 'vertical';
@property() resize: 'none' | 'vertical' | 'auto' = 'vertical';
/** Disables the textarea. */
disabled = false;
@property({ type: Boolean, reflect: true }) disabled = false;
/** Makes the textarea readonly. */
readonly = false;
@property({ type: Boolean, reflect: true }) readonly = false;
/** The minimum length of input that will be considered valid. */
minlength: number;
@property({ type: Number }) minlength: number;
/** The maximum length of input that will be considered valid. */
maxlength: number;
@property({ type: Number }) maxlength: number;
/** A pattern to validate input against. */
pattern: string;
@property() pattern: string;
/** Makes the textarea a required field. */
required = false;
@property({ type: Boolean, reflect: true }) required = false;
/**
* This will be true when the control is in an invalid state. Validity is determined by props such as `type`,
* `required`, `minlength`, and `maxlength` using the browser's constraint validation API.
*/
invalid = false;
@property({ type: Boolean, reflect: true }) invalid = false;
/** The textarea's autocaptialize attribute. */
autocapitalize: string;
@property() autocapitalize: string;
/** The textarea's autocorrect attribute. */
autocorrect: string;
@property() autocorrect: string;
/** The textarea's autocomplete attribute. */
autocomplete: string;
@property() autocomplete: string;
/** The textarea's autofocus attribute. */
autofocus: boolean;
@property({ type: Boolean }) autofocus: boolean;
/** Enables spell checking on the textarea. */
spellcheck: boolean;
@property({ type: Boolean }) spellcheck: boolean;
/** The textarea's inputmode attribute. */
inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
@property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
onConnect() {
/** Emitted when the control's value changes. */
@event('sl-change') slChange: EventEmitter<void>;
/** Emitted when the control receives input. */
@event('sl-input') slInput: EventEmitter<void>;
/** Emitted when the control gains focus. */
@event('sl-focus') slFocus: EventEmitter<void>;
/** Emitted when the control loses focus. */
@event('sl-blur') slBlur: EventEmitter<void>;
connectedCallback() {
super.connectedCallback();
this.handleSlotChange = this.handleSlotChange.bind(this);
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
this.handleSlotChange();
}
onReady() {
firstUpdated() {
this.setTextareaHeight();
this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight());
this.resizeObserver.observe(this.input);
}
onDisconnect() {
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (changedProps.has('help-text') || changedProps.has('label')) {
this.handleSlotChange();
}
if (changedProps.has('rows')) {
this.setTextareaHeight();
}
if (changedProps.has('value')) {
this.invalid = !this.input.checkValidity();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver.unobserve(this.input);
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
}
@ -181,14 +185,14 @@ export default class SlTextarea extends Shoemaker {
if (this.value !== this.input.value) {
this.value = this.input.value;
this.emit('sl-input');
this.slInput.emit();
}
if (this.value !== this.input.value) {
this.value = this.input.value;
this.setTextareaHeight();
this.emit('sl-input');
this.emit('sl-change');
this.slInput.emit();
this.slChange.emit();
}
}
@ -205,23 +209,23 @@ export default class SlTextarea extends Shoemaker {
handleChange() {
this.value = this.input.value;
this.emit('sl-change');
this.slChange.emit();
}
handleInput() {
this.value = this.input.value;
this.setTextareaHeight();
this.emit('sl-input');
this.slInput.emit();
}
handleBlur() {
this.hasFocus = false;
this.emit('sl-blur');
this.slBlur.emit();
}
handleFocus() {
this.hasFocus = true;
this.emit('sl-focus');
this.slFocus.emit();
}
handleSlotChange() {
@ -238,22 +242,6 @@ export default class SlTextarea extends Shoemaker {
}
}
watchHelpText() {
this.handleSlotChange();
}
watchLabel() {
this.handleSlotChange();
}
watchRows() {
this.setTextareaHeight();
}
watchValue() {
this.invalid = !this.input.checkValidity();
}
render() {
return renderFormControl(
{
@ -285,29 +273,28 @@ export default class SlTextarea extends Shoemaker {
>
<textarea
part="textarea"
ref=${(el: HTMLTextAreaElement) => (this.input = el)}
id=${this.inputId}
class="textarea__control"
name=${this.name}
placeholder=${this.placeholder}
disabled=${this.disabled ? true : null}
readonly=${this.readonly ? true : null}
rows=${this.rows}
minlength=${this.minlength}
maxlength=${this.maxlength}
.value=${this.value}
autocapitalize=${this.autocapitalize}
autocorrect=${this.autocorrect}
autofocus=${this.autofocus}
spellcheck=${this.spellcheck}
required=${this.required ? true : null}
inputmode=${this.inputmode}
?disabled=${this.disabled}
?readonly=${this.readonly}
?required=${this.required}
placeholder=${ifDefined(this.placeholder)}
rows=${ifDefined(this.rows)}
minlength=${ifDefined(this.minlength)}
maxlength=${ifDefined(this.maxlength)}
autocapitalize=${ifDefined(this.autocapitalize)}
autocorrect=${ifDefined(this.autocorrect)}
autofocus=${ifDefined(this.autofocus)}
spellcheck=${ifDefined(this.spellcheck)}
inputmode=${ifDefined(this.inputmode)}
aria-labelledby=${this.labelId}
onchange=${this.handleChange.bind(this)}
oninput=${this.handleInput.bind(this)}
onfocus=${this.handleFocus.bind(this)}
onblur=${this.handleBlur.bind(this)}
/>
@change=${this.handleChange.bind(this)}
@input=${this.handleInput.bind(this)}
@focus=${this.handleFocus.bind(this)}
@blur=${this.handleBlur.bind(this)}
></textarea>
</div>
`
);

Wyświetl plik

@ -1,4 +1,6 @@
import { classMap, html, Shoemaker } from '@shoelace-style/shoemaker';
import { LitElement, customElement, html, property, query, unsafeCSS } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter } from '../../internal/event';
import styles from 'sass:./tooltip.scss';
import Popover from '../../internal/popover';
@ -12,33 +14,28 @@ let id = 0;
* @slot content - The tooltip's content. Alternatively, you can use the content prop.
*
* @part base - The component's base wrapper.
*
* @emit sl-show - Emitted when the tooltip begins to show. Calling `event.preventDefault()` will prevent it from being shown.
* @emit sl-after-show - Emitted after the tooltip has shown and all transitions are complete.
* @emit sl-hide - Emitted when the tooltip begins to hide. Calling `event.preventDefault()` will prevent it from being hidden.
* @emit sl-after-hide - Emitted after the tooltip has hidden and all transitions are complete.
*/
export default class SlTooltip extends Shoemaker {
static tag = 'sl-tooltip';
static props = ['content', 'placement', 'disabled', 'distance', 'open', 'skidding', 'trigger'];
static reflect = ['disabled', 'open'];
static styles = styles;
@customElement('sl-tooltip')
export class SlTooltip extends LitElement {
static styles = unsafeCSS(styles);
@query('.tooltip-positioner') positioner: HTMLElement;
@query('.tooltip') tooltip: HTMLElement;
private componentId = `tooltip-${++id}`;
private isVisible = false;
private popover: Popover;
private positioner: HTMLElement;
private target: HTMLElement;
private tooltip: HTMLElement;
private popover: Popover;
private isVisible = false;
/** The tooltip's content. Alternatively, you can use the content slot. */
content = '';
@property() content = '';
/**
* The preferred placement of the tooltip. Note that the actual placement may vary as needed to keep the tooltip
* inside of the viewport.
*/
placement:
@property() placement:
| 'top'
| 'top-start'
| 'top-end'
@ -53,25 +50,37 @@ export default class SlTooltip extends Shoemaker {
| 'left-end' = 'top';
/** Disables the tooltip so it won't show when triggered. */
disabled = false;
@property({ type: Boolean }) disabled = false;
/** The distance in pixels from which to offset the tooltip away from its target. */
distance = 10;
@property({ type: Number }) distance = 10;
/** Indicates whether or not the tooltip is open. You can use this in lieu of the show/hide methods. */
open = false;
@property({ type: Boolean }) open = false;
/** The distance in pixels from which to offset the tooltip along its target. */
skidding = 0;
@property({ type: Number }) skidding = 0;
/**
* Controls how the tooltip is activated. Possible options include `click`, `hover`, `focus`, and `manual`. Multiple
* options can be passed by separating them with a space. When manual is used, the tooltip must be activated
* programmatically.
*/
trigger: string = 'hover focus';
@property() trigger = 'hover focus';
onReady() {
/** Emitted when the tooltip begins to show. Calling `event.preventDefault()` will prevent it from being shown. */
@event('sl-show') slShow: EventEmitter<void>;
/** Emitted after the tooltip has shown and all transitions are complete. */
@event('sl-after-show') slAfterShow: EventEmitter<void>;
/** Emitted when the tooltip begins to hide. Calling `event.preventDefault()` will prevent it from being hidden. */
@event('sl-hide') slHide: EventEmitter<void>;
/** Emitted after the tooltip has hidden and all transitions are complete. */
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
firstUpdated() {
this.target = this.getTarget();
this.popover = new Popover(this.target, this.positioner);
this.syncOptions();
@ -90,7 +99,20 @@ export default class SlTooltip extends Shoemaker {
}
}
onDisconnect() {
update(changedProps: Map<string, any>) {
super.update(changedProps);
if (['placement', 'disabled', 'distance', 'skidding'].find(prop => changedProps.has(prop))) {
this.syncOptions();
}
if (changedProps.has('open')) {
this.open ? this.show() : this.hide();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.popover.destroy();
this.removeEventListener('blur', this.handleBlur, true);
this.removeEventListener('click', this.handleClick, true);
@ -104,7 +126,7 @@ export default class SlTooltip extends Shoemaker {
return;
}
const slShow = this.emit('sl-show');
const slShow = this.slShow.emit();
if (slShow.defaultPrevented) {
this.open = false;
return;
@ -122,7 +144,7 @@ export default class SlTooltip extends Shoemaker {
return;
}
const slHide = this.emit('sl-hide');
const slHide = this.slHide.emit();
if (slHide.defaultPrevented) {
this.open = true;
return;
@ -202,45 +224,26 @@ export default class SlTooltip extends Shoemaker {
}
syncOptions() {
this.popover.setOptions({
placement: this.placement,
distance: this.distance,
skidding: this.skidding,
transitionElement: this.tooltip,
onAfterHide: () => this.emit('sl-after-hide'),
onAfterShow: () => this.emit('sl-after-show')
});
}
watchPlacement() {
this.syncOptions();
}
watchDisabled() {
this.syncOptions();
}
watchDistance() {
this.syncOptions();
}
watchOpen() {
this.open ? this.show() : this.hide();
}
watchSkidding() {
this.syncOptions();
if (this.popover) {
this.popover.setOptions({
placement: this.placement,
distance: this.distance,
skidding: this.skidding,
transitionElement: this.tooltip,
onAfterHide: () => this.slAfterHide.emit(),
onAfterShow: () => this.slAfterShow.emit()
});
}
}
render() {
return html`
<slot onslotchange=${this.handleSlotChange.bind(this)} />
<slot @slotchange=${this.handleSlotChange.bind(this)}></slot>
${!this.disabled
? html`
<div ref=${(el: HTMLElement) => (this.positioner = el)} class="tooltip-positioner">
<div class="tooltip-positioner">
<div
ref=${(el: HTMLElement) => (this.tooltip = el)}
part="base"
id=${this.componentId}
class=${classMap({

Wyświetl plik

@ -0,0 +1,51 @@
// @event decorator
export function event(eventName?: string) {
// Legacy TS Decorator
function legacyEvent(descriptor: PropertyDescriptor, protoOrDescriptor: {}, name: PropertyKey) {
Object.defineProperty(protoOrDescriptor, name, descriptor);
}
// TC39 Decorators proposal
function standardEvent(descriptor: PropertyDescriptor, element: { key: string }) {
return {
kind: 'method',
placement: 'prototype',
key: element.key,
descriptor
};
}
return (protoOrDescriptor: any, name: string): any => {
const descriptor = {
get(this: HTMLElement) {
return new EventEmitter(this, eventName || (name !== undefined ? name : protoOrDescriptor.key));
},
enumerable: true,
configurable: true
};
return name !== undefined
? legacyEvent(descriptor, protoOrDescriptor, name)
: standardEvent(descriptor, protoOrDescriptor);
};
}
// EventEmitter to use with the @event decorator
export class EventEmitter<T> {
constructor(private target: HTMLElement, private eventName: string) {}
emit(eventOptions?: CustomEventInit) {
const event = new CustomEvent<T>(
this.eventName,
Object.assign(
{
bubbles: true,
cancelable: true,
composed: true,
detail: {}
},
eventOptions
)
);
this.target.dispatchEvent(event);
return event;
}
}

Wyświetl plik

@ -1,4 +1,5 @@
import { classMap, html, Hole } from '@shoelace-style/shoemaker';
import { html, TemplateResult } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
export interface FormControlProps {
/** The input id, used to map the input to the label */
@ -29,9 +30,9 @@ export interface FormControlProps {
onLabelClick?: (event: MouseEvent) => void;
}
export const renderFormControl = (props: FormControlProps, input: Hole | string) => {
const hasLabel = props.label ? true : props.hasLabelSlot;
const hasHelpText = props.helpText ? true : props.hasHelpTextSlot;
export const renderFormControl = (props: FormControlProps, input: TemplateResult) => {
const hasLabel = props.label ? true : !!props.hasLabelSlot;
const hasHelpText = props.helpText ? true : !!props.hasHelpTextSlot;
return html`
<div
@ -51,7 +52,7 @@ export const renderFormControl = (props: FormControlProps, input: Hole | string)
class="form-control__label"
for=${props.inputId}
aria-hidden=${hasLabel ? 'false' : 'true'}
onclick=${(event: MouseEvent) => (props.onLabelClick ? props.onLabelClick(event) : null)}
@click=${(event: MouseEvent) => (props.onLabelClick ? props.onLabelClick(event) : null)}
>
<slot name="label">${props.label}</slot>
</label>

Wyświetl plik

@ -1,46 +1,46 @@
export * from './utilities';
export { default as SlAlert } from './components/alert/alert';
export { default as SlAnimation } from './components/animation/animation';
export { default as SlAvatar } from './components/avatar/avatar';
export { default as SlBadge } from './components/badge/badge';
export { default as SlButton } from './components/button/button';
export { default as SlButtonGroup } from './components/button-group/button-group';
export { default as SlCard } from './components/card/card';
export { default as SlCheckbox } from './components/checkbox/checkbox';
export { default as SlColorPicker } from './components/color-picker/color-picker';
export { default as SlDetails } from './components/details/details';
export { default as SlDialog } from './components/dialog/dialog';
export { default as SlDrawer } from './components/drawer/drawer';
export { default as SlDropdown } from './components/dropdown/dropdown';
export { default as SlForm } from './components/form/form';
export { default as SlFormatBytes } from './components/format-bytes/format-bytes';
export { default as SlFormatDate } from './components/format-date/format-date';
export { default as SlFormatNumber } from './components/format-number/format-number';
export { default as SlIcon } from './components/icon/icon';
export { default as SlIconButton } from './components/icon-button/icon-button';
export { default as SlImageComparer } from './components/image-comparer/image-comparer';
export { default as SlInclude } from './components/include/include';
export { default as SlInput } from './components/input/input';
export { default as SlMenu } from './components/menu/menu';
export { default as SlMenuDivider } from './components/menu-divider/menu-divider';
export { default as SlMenuItem } from './components/menu-item/menu-item';
export { default as SlMenuLabel } from './components/menu-label/menu-label';
export { default as SlProgressBar } from './components/progress-bar/progress-bar';
export { default as SlProgressRing } from './components/progress-ring/progress-ring';
export { default as SlRadio } from './components/radio/radio';
export { default as SlRange } from './components/range/range';
export { default as SlRating } from './components/rating/rating';
export { default as SlRelativeTime } from './components/relative-time/relative-time';
export { default as SlResizeObserver } from './components/resize-observer/resize-observer';
export { default as SlResponsiveEmbed } from './components/responsive-embed/responsive-embed';
export { default as SlSelect } from './components/select/select';
export { default as SlSkeleton } from './components/skeleton/skeleton';
export { default as SlSpinner } from './components/spinner/spinner';
export { default as SlSwitch } from './components/switch/switch';
export { default as SlTab } from './components/tab/tab';
export { default as SlTabGroup } from './components/tab-group/tab-group';
export { default as SlTabPanel } from './components/tab-panel/tab-panel';
export { default as SlTag } from './components/tag/tag';
export { default as SlTextarea } from './components/textarea/textarea';
export { default as SlTooltip } from './components/tooltip/tooltip';
export { SlAlert } from './components/alert/alert';
export { SlAnimation } from './components/animation/animation';
export { SlAvatar } from './components/avatar/avatar';
export { SlBadge } from './components/badge/badge';
export { SlButton } from './components/button/button';
export { SlButtonGroup } from './components/button-group/button-group';
export { SlCard } from './components/card/card';
export { SlCheckbox } from './components/checkbox/checkbox';
export { SlColorPicker } from './components/color-picker/color-picker';
export { SlDetails } from './components/details/details';
export { SlDialog } from './components/dialog/dialog';
export { SlDrawer } from './components/drawer/drawer';
export { SlDropdown } from './components/dropdown/dropdown';
export { SlForm } from './components/form/form';
export { SlFormatBytes } from './components/format-bytes/format-bytes';
export { SlFormatDate } from './components/format-date/format-date';
export { SlFormatNumber } from './components/format-number/format-number';
export { SlIcon } from './components/icon/icon';
export { SlIconButton } from './components/icon-button/icon-button';
export { SlImageComparer } from './components/image-comparer/image-comparer';
export { SlInclude } from './components/include/include';
export { SlInput } from './components/input/input';
export { SlMenu } from './components/menu/menu';
export { SlMenuDivider } from './components/menu-divider/menu-divider';
export { SlMenuItem } from './components/menu-item/menu-item';
export { SlMenuLabel } from './components/menu-label/menu-label';
export { SlProgressBar } from './components/progress-bar/progress-bar';
export { SlProgressRing } from './components/progress-ring/progress-ring';
export { SlRadio } from './components/radio/radio';
export { SlRange } from './components/range/range';
export { SlRating } from './components/rating/rating';
export { SlRelativeTime } from './components/relative-time/relative-time';
export { SlResizeObserver } from './components/resize-observer/resize-observer';
export { SlResponsiveEmbed } from './components/responsive-embed/responsive-embed';
export { SlSelect } from './components/select/select';
export { SlSkeleton } from './components/skeleton/skeleton';
export { SlSpinner } from './components/spinner/spinner';
export { SlSwitch } from './components/switch/switch';
export { SlTab } from './components/tab/tab';
export { SlTabGroup } from './components/tab-group/tab-group';
export { SlTabPanel } from './components/tab-panel/tab-panel';
export { SlTag } from './components/tag/tag';
export { SlTextarea } from './components/textarea/textarea';
export { SlTooltip } from './components/tooltip/tooltip';

Wyświetl plik

@ -64,7 +64,7 @@
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */