From 2f4d93700abd349257adebef47023909044c51c3 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Sat, 6 Mar 2021 12:01:39 -0500 Subject: [PATCH] migrate to LitElement --- README.md | 2 +- docs/assets/plugins/metadata/metadata.js | 4 +- docs/components/icon-button.md | 2 - docs/components/rating.md | 7 +- docs/getting-started/changelog.md | 9 +- docs/getting-started/overview.md | 2 +- package-lock.json | 18 +- package.json | 5 +- scripts/make-metadata.cjs | 21 +- src/components/alert/alert.ts | 68 +++-- src/components/animation/animation.ts | 187 +++++++------- src/components/avatar/avatar.ts | 23 +- src/components/badge/badge.ts | 19 +- src/components/button-group/button-group.scss | 6 +- src/components/button-group/button-group.ts | 32 +-- src/components/button/button.ts | 108 ++++---- src/components/card/card.ts | 27 +- src/components/checkbox/checkbox.ts | 84 +++--- src/components/color-picker/color-picker.ts | 182 +++++++------ src/components/details/details.ts | 69 ++--- src/components/dialog/dialog.ts | 98 ++++--- src/components/drawer/drawer.ts | 95 ++++--- src/components/dropdown/dropdown.ts | 78 +++--- src/components/form/form.ts | 50 ++-- src/components/format-bytes/format-bytes.ts | 14 +- src/components/format-date/format-date.ts | 48 ++-- src/components/format-number/format-number.ts | 42 +-- src/components/icon-button/icon-button.ts | 36 +-- src/components/icon/icon.ts | 70 ++--- src/components/icon/library.ts | 8 +- .../image-comparer/image-comparer.ts | 59 +++-- src/components/include/include.ts | 45 ++-- src/components/input/input.ts | 239 ++++++++---------- src/components/menu-divider/menu-divider.ts | 10 +- src/components/menu-item/menu-item.ts | 39 ++- src/components/menu-label/menu-label.ts | 10 +- src/components/menu/menu.ts | 33 +-- src/components/progress-bar/progress-bar.ts | 17 +- src/components/progress-ring/progress-ring.ts | 34 +-- src/components/radio/radio.ts | 84 +++--- src/components/range/range.scss | 2 +- src/components/range/range.ts | 131 +++++----- src/components/rating/rating.ts | 77 +++--- src/components/relative-time/relative-time.ts | 58 ++--- .../resize-observer/resize-observer.ts | 24 +- .../responsive-embed/responsive-embed.ts | 29 ++- src/components/select/select.ts | 214 ++++++++-------- src/components/skeleton/skeleton.ts | 14 +- src/components/spinner/spinner.ts | 8 +- src/components/switch/switch.ts | 80 +++--- src/components/tab-group/tab-group.ts | 93 +++---- src/components/tab-panel/tab-panel.ts | 18 +- src/components/tab/tab.ts | 37 +-- src/components/tag/tag.ts | 33 +-- src/components/textarea/textarea.ts | 195 +++++++------- src/components/tooltip/tooltip.ts | 117 ++++----- src/internal/event.ts | 51 ++++ src/internal/form-control.ts | 11 +- src/shoelace.ts | 88 +++---- tsconfig.json | 2 +- 60 files changed, 1656 insertions(+), 1610 deletions(-) create mode 100644 src/internal/event.ts diff --git a/README.md b/README.md index 5160a45f..7102c33a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/assets/plugins/metadata/metadata.js b/docs/assets/plugins/metadata/metadata.js index 224a742a..11b6d630 100644 --- a/docs/assets/plugins/metadata/metadata.js +++ b/docs/assets/plugins/metadata/metadata.js @@ -56,6 +56,7 @@ Event Description + Details @@ -65,6 +66,7 @@ ${escapeHtml(event.name)} ${escapeHtml(event.description)} + ${escapeHtml(event.details)} ` ) @@ -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 <${tag}>. ${createDependenciesList(component.tag, metadata.components)} diff --git a/docs/components/icon-button.md b/docs/components/icon-button.md index 9afc9f87..d363c0d8 100644 --- a/docs/components/icon-button.md +++ b/docs/components/icon-button.md @@ -8,8 +8,6 @@ For a full list of icons that come bundled with Shoelace, refer to the [icon com ```html preview - - ``` ## Examples diff --git a/docs/components/rating.md b/docs/components/rating.md index b5624ec9..c848d244 100644 --- a/docs/components/rating.md +++ b/docs/components/rating.md @@ -53,7 +53,12 @@ Use the `disable` attribute to disable the rating. ### Custom Icons ```html preview - + + + ``` ### Value-based Icons diff --git a/docs/getting-started/changelog.md b/docs/getting-started/changelog.md index 9d220bbd..153be7db 100644 --- a/docs/getting-started/changelog.md +++ b/docs/getting-started/changelog.md @@ -6,16 +6,19 @@ Components with the Experimental 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. diff --git a/docs/getting-started/overview.md b/docs/getting-started/overview.md index 6d66a74e..bf82c73a 100644 --- a/docs/getting-started/overview.md +++ b/docs/getting-started/overview.md @@ -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/) diff --git a/package-lock.json b/package-lock.json index 755fc586..8229d373 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 924c7562..4250c790 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/make-metadata.cjs b/scripts/make-metadata.cjs index f2002cf2..435fa2cc 100644 --- a/scripts/make-metadata.cjs +++ b/scripts/make-metadata.cjs @@ -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) diff --git a/src/components/alert/alert.ts b/src/components/alert/alert.ts index 3f4809c4..8f1462ab 100644 --- a/src/components/alert/alert.ts +++ b/src/components/alert/alert.ts @@ -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; + + /** Emitted after the alert opens and all transitions are complete. */ + @event('sl-after-show') slAfterShow: EventEmitter; + + /** Emitted when the alert closes. Calling `event.preventDefault()` will prevent it from being closed. */ + @event('sl-hide') slHide: EventEmitter; + + /** Emitted after the alert closes and all transitions are complete. */ + @event('sl-after-hide') slAfterHide: EventEmitter; + + 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)} > - + - + ${this.closable ? html` - + ` : ''} diff --git a/src/components/animation/animation.ts b/src/components/animation/animation.ts index 4d4e8cac..89860164 100644 --- a/src/components/animation/animation.ts +++ b/src/components/animation/animation.ts @@ -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; + /** 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; + + /** Emitted when the animation finishes. */ + @event('sl-finish') slFinish: EventEmitter; + + /** Emitted when the animation starts or restarts. */ + @event('sl-start') slStart: EventEmitter; + + connectedCallback() { + super.connectedCallback(); this.createAnimation(); } - onDisconnect() { + disconnectedCallback() { + super.disconnectedCallback(); this.destroyAnimation(); } + update(changedProps: Map) { + 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` - (this.defaultSlot = el)} onslotchange=${this.handleSlotChange.bind(this)} /> - `; + return html` `; } } diff --git a/src/components/avatar/avatar.ts b/src/components/avatar/avatar.ts index 77b6686c..8eaa2eb1 100644 --- a/src/components/avatar/avatar.ts +++ b/src/components/avatar/avatar.ts @@ -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` - + ` : ''} diff --git a/src/components/badge/badge.ts b/src/components/badge/badge.ts index 3aff024f..1f78cfa5 100644 --- a/src/components/badge/badge.ts +++ b/src/components/badge/badge.ts @@ -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" > - + `; } diff --git a/src/components/button-group/button-group.scss b/src/components/button-group/button-group.scss index bdcd9bf5..b454a220 100644 --- a/src/components/button-group/button-group.scss +++ b/src/components/button-group/button-group.scss @@ -10,10 +10,6 @@ position: relative; } -::slotted(.sl-hover) { +::slotted(.sl-focus) { z-index: 1; } - -::slotted(.sl-focus) { - z-index: 2; -} diff --git a/src/components/button-group/button-group.ts b/src/components/button-group/button-group.ts index 72c0ea5a..7bf6953b 100644 --- a/src/components/button-group/button-group.ts +++ b/src/components/button-group/button-group.ts @@ -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`
(this.buttonGroup = el)} part="base" class="button-group" aria-label=${this.label} + @focusout=${this.handleBlur} + @focusin=${this.handleFocus} > - +
`; } diff --git a/src/components/button/button.ts b/src/components/button/button.ts index 11855ef1..76103d06 100644 --- a/src/components/button/button.ts +++ b/src/components/button/button.ts @@ -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 `` with this `href` instead of a ` @@ -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} diff --git a/src/components/card/card.ts b/src/components/card/card.ts index 9ff7a6bb..cf45a852 100644 --- a/src/components/card/card.ts +++ b/src/components/card/card.ts @@ -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 { })} >
- +
- +
- +
`; diff --git a/src/components/checkbox/checkbox.ts b/src/components/checkbox/checkbox.ts index f5d180ea..f966a1c0 100644 --- a/src/components/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox.ts @@ -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; + + /** Emitted when the control's checked state changes. */ + @event('sl-change') slChange: EventEmitter; + + /** Emitted when the control gains focus. */ + @event('sl-focus') slFocus: EventEmitter; + + 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} > ${this.checked @@ -159,25 +168,24 @@ export default class SlCheckbox extends Shoemaker { : ''} (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} /> - + `; diff --git a/src/components/color-picker/color-picker.ts b/src/components/color-picker/color-picker.ts index d690147c..eb6fc41c 100644 --- a/src/components/color-picker/color-picker.ts +++ b/src/components/color-picker/color-picker.ts @@ -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; + + /** Emitted when the color picker opens. Calling `event.preventDefault()` will prevent it from being opened. */ + @event('sl-show') slShow: EventEmitter; + + /** Emitted after the color picker opens and all transitions are complete. */ + @event('sl-after-show') slAfterShow: EventEmitter; + + /** Emitted when the color picker closes. Calling `event.preventDefault()` will prevent it from being closed. */ + @event('sl-hide') slHide: EventEmitter; + + /** Emitted after the color picker closes and all transitions are complete. */ + @event('sl-after-hide') slAfterHide: EventEmitter; + + 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} > + @keydown=${this.handleGridKeyDown} + >
@@ -657,8 +652,8 @@ export default class SlColorPicker extends Shoemaker {
+ @keydown=${this.handleHueKeyDown} + >
${this.opacity @@ -682,19 +677,19 @@ export default class SlColorPicker extends Shoemaker {
+ >
+ @keydown=${this.handleAlphaKeyDown} + >
` : ''}
(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} + > ${!this.noFormatToggle ? html` - + ${this.setLetterCase(this.format)} ` @@ -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)} > -
+
`; })} @@ -795,15 +788,14 @@ export default class SlColorPicker extends Shoemaker { // Render as a dropdown return html` (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} > ${colorPicker} `; diff --git a/src/components/details/details.ts b/src/components/details/details.ts index 7e68ac6a..46521649 100644 --- a/src/components/details/details.ts +++ b/src/components/details/details.ts @@ -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; + + /** Emitted after the details opens and all transitions are complete. */ + @event('after-show') slAfterShow: EventEmitter; + + /** Emitted when the details closes. Calling `event.preventDefault()` will prevent it from being closed. */ + @event('sl-hide') slHide: EventEmitter; + + /** Emitted after the details closes and all transitions are complete. */ + @event('after-hide') slAfterHide: EventEmitter; + + 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`
(this.details = el)} part="base" class=${classMap({ details: true, @@ -167,7 +175,6 @@ export default class SlDetails extends Shoemaker { })} >
(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} >
${this.summary}
- +
-
(this.body = el)} - class="details__body" - ontransitionend=${this.handleBodyTransitionEnd.bind(this)} - > +
- +
diff --git a/src/components/dialog/dialog.ts b/src/components/dialog/dialog.ts index 623d98d4..d13b08c7 100644 --- a/src/components/dialog/dialog.ts +++ b/src/components/dialog/dialog.ts @@ -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; + + /** Emitted after the dialog opens and all transitions are complete. */ + @event('sl-after-show') slAfterShow: EventEmitter; + + /** Emitted when the dialog closes. Calling `event.preventDefault()` will prevent it from being closed. */ + @event('sl-hide') slHide: EventEmitter; + + /** Emitted after the dialog closes and all transitions are complete. */ + @event('sl-after-hide') slAfterHide: EventEmitter; + + /** + * 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; + + /** Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the dialog from closing. */ + @event('sl-overlay-dismiss') slOverlayDismiss: EventEmitter; + + 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`
(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} > -
+
(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}" + > ` : ''}
- +
- +
diff --git a/src/components/drawer/drawer.ts b/src/components/drawer/drawer.ts index 3f4e8b85..369f7b45 100644 --- a/src/components/drawer/drawer.ts +++ b/src/components/drawer/drawer.ts @@ -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; + + /** Emitted after the drawer opens and all transitions are complete. */ + @event('sl-after-show') slAfterShow: EventEmitter; + + /** Emitted when the drawer closes. Calling `event.preventDefault()` will prevent it from being closed. */ + @event('sl-hide') slHide: EventEmitter; + + /** Emitted after the drawer closes and all transitions are complete. */ + @event('sl-after-hide') slAfterHide: EventEmitter; + + /** 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; + + /** Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the drawer from closing. */ + @event('sl-overlay-dismiss') slOverlayDismiss: EventEmitter; + + 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`
(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} > -
+
(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} + > ` : ''}
- +
- +
diff --git a/src/components/dropdown/dropdown.ts b/src/components/dropdown/dropdown.ts index 99f5042b..35949274 100644 --- a/src/components/dropdown/dropdown.ts +++ b/src/components/dropdown/dropdown.ts @@ -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; + + /** Emitted after the dropdown opens and all transitions are complete. */ + @event('sl-after-show') slAfterShow: EventEmitter; + + /** Emitted when the dropdown closes. Calling `event.preventDefault()` will prevent it from being closed. */ + @event('sl-hide') slHide: EventEmitter; + + /** Emitted after the dropdown closes and all transitions are complete. */ + @event('sl-after-hide') slAfterHide: EventEmitter; 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 { (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} > - + -
(this.positioner = el)} class="dropdown__positioner"> +