diff --git a/docs/components/select.md b/docs/components/select.md index 5101dd269..356aa60e1 100644 --- a/docs/components/select.md +++ b/docs/components/select.md @@ -15,177 +15,3 @@ Selects allow you to choose one or more items from a dropdown menu. Option 6 ``` - -?> This component doesn't work with standard forms. Use [``](/components/form) instead. - -## Examples - -### Placeholders - -Use the `placeholder` attribute to add a placeholder. - -```html preview - - Option 1 - Option 2 - Option 3 - -``` - -### Clearable - -Use the `clearable` attribute to make the control clearable. - -```html preview - - Option 1 - Option 2 - Option 3 - -``` - -### Pill - -Use the `pill` prop to give selects rounded edges. - -```html preview - - Option 1 - Option 2 - Option 3 - -``` - -### Disabled - -Use the `disabled` prop to disable a select. - -```html preview - - Option 1 - Option 2 - Option 3 - -``` - -### Multiple - -To allow multiple options to be selected, use the `multiple` attribute. It's a good practice to use `clearable` when this option is enabled. When using this option, `value` will be an array instead of a string. - -```html preview - - Option 1 - Option 2 - Option 3 - - Option 4 - Option 5 - Option 6 - -``` - -### Grouping Options - -Options can be grouped visually using menu labels and menu dividers. - -```html preview - - Group 1 - Option 1 - Option 2 - Option 3 - - Group 2 - Option 4 - Option 5 - Option 6 - -``` - -### Sizes - -Use the `size` attribute to change a select's size. - -```html preview - - Option 1 - Option 2 - Option 3 - - -
- - - Option 1 - Option 2 - Option 3 - - -
- - - Option 1 - Option 2 - Option 3 - -``` - -### Selecting Options Programmatically - -The `value` prop is bound to the current selection. As the selection changes, so will the value. To programmatically manage the selection, update the `value` property. - -```html preview -
- - Option 1 - Option 2 - Option 3 - - -
- - Set 1 - Set 2 - Set 3 -
- - -``` - -### Labels - -Use the `label` attribute to give the select an accessible label. For labels that contain HTML, use the `label` slot instead. - -```html preview - - Option 1 - Option 2 - Option 3 - -``` - -### Help Text - -Add descriptive help text to a select with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead. - -```html preview - - Novice - Intermediate - Advanced - -``` - -[component-metadata:sl-select] diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 97c9ac996..5a1858105 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -11,6 +11,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis - Added `?` to optional arguments in methods tables - Added the `scrollPosition()` method to `sl-textarea` to get/set scroll position - Fixed a bug in `sl-tab-group` where scrollable tab icons were not displaying correctly +- Fixed lifecycle bugs in a number of components [#451](https://github.com/shoelace-style/shoelace/issues/451) - Removed `fill: both` from internal animate utility so styles won't "stick" by default [#450](https://github.com/shoelace-style/shoelace/issues/450) ## 2.0.0-beta.42 diff --git a/src/components/alert/alert.ts b/src/components/alert/alert.ts index df2245605..08d8531ec 100644 --- a/src/components/alert/alert.ts +++ b/src/components/alert/alert.ts @@ -64,13 +64,12 @@ export default class SlAlert extends LitElement { /** Emitted after the alert closes and all transitions are complete. */ @event('sl-after-hide') slAfterHide: EventEmitter; - async firstUpdated() { + firstUpdated() { // Set initial visibility this.base.hidden = !this.open; - // Set the initialized flag after the first update is complete - await this.updateComplete; - this.hasInitialized = true; + // Set the initialized flag after the first render is complete + this.updateComplete.then(() => (this.hasInitialized = true)); } /** Shows the alert. */ diff --git a/src/components/details/details.ts b/src/components/details/details.ts index 42d117516..43a63601a 100644 --- a/src/components/details/details.ts +++ b/src/components/details/details.ts @@ -60,15 +60,17 @@ export default class SlDetails extends LitElement { /** Emitted after the details closes and all transitions are complete. */ @event('sl-after-hide') slAfterHide: EventEmitter; - async firstUpdated() { - focusVisible.observe(this.details); + connectedCallback() { + super.connectedCallback(); + this.updateComplete.then(() => focusVisible.observe(this.details)); + } + firstUpdated() { this.body.hidden = !this.open; this.body.style.height = this.open ? 'auto' : '0'; - // Set the initialized flag after the first update is complete - await this.updateComplete; - this.hasInitialized = true; + // Set the initialized flag after the first render is complete + this.updateComplete.then(() => (this.hasInitialized = true)); } disconnectedCallback() { diff --git a/src/components/dialog/dialog.ts b/src/components/dialog/dialog.ts index 980cc6c65..ba37b4eef 100644 --- a/src/components/dialog/dialog.ts +++ b/src/components/dialog/dialog.ts @@ -103,13 +103,12 @@ export default class SlDialog extends LitElement { this.handleSlotChange(); } - async firstUpdated() { + firstUpdated() { // Set initial visibility this.dialog.hidden = !this.open; - // Set the initialized flag after the first update is complete - await this.updateComplete; - this.hasInitialized = true; + // Set the initialized flag after the first render is complete + this.updateComplete.then(() => (this.hasInitialized = true)); } disconnectedCallback() { diff --git a/src/components/drawer/drawer.ts b/src/components/drawer/drawer.ts index fccd14e84..f8673e53a 100644 --- a/src/components/drawer/drawer.ts +++ b/src/components/drawer/drawer.ts @@ -117,13 +117,12 @@ export default class SlDrawer extends LitElement { this.handleSlotChange(); } - async firstUpdated() { + firstUpdated() { // Set initial visibility this.drawer.hidden = !this.open; - // Set the initialized flag after the first update is complete - await this.updateComplete; - this.hasInitialized = true; + // Set the initialized flag after the first render is complete + this.updateComplete.then(() => (this.hasInitialized = true)); } disconnectedCallback() { diff --git a/src/components/dropdown/dropdown.ts b/src/components/dropdown/dropdown.ts index cbf735c6f..d8a28d73f 100644 --- a/src/components/dropdown/dropdown.ts +++ b/src/components/dropdown/dropdown.ts @@ -128,13 +128,12 @@ export default class SlDropdown extends LitElement { }); } - async firstUpdated() { + firstUpdated() { // Set initial visibility this.panel.hidden = !this.open; - // Set the initialized flag after the first update is complete - await this.updateComplete; - this.hasInitialized = true; + // Set the initialized flag after the first render is complete + this.updateComplete.then(() => (this.hasInitialized = true)); } disconnectedCallback() { diff --git a/src/components/icon-button/icon-button.ts b/src/components/icon-button/icon-button.ts index 8ceb1cdcf..5b51e90fe 100644 --- a/src/components/icon-button/icon-button.ts +++ b/src/components/icon-button/icon-button.ts @@ -37,8 +37,9 @@ export default class SlIconButton extends LitElement { /** Disables the button. */ @property({ type: Boolean, reflect: true }) disabled = false; - firstUpdated() { - focusVisible.observe(this.button); + connectedCallback() { + super.connectedCallback(); + this.updateComplete.then(() => focusVisible.observe(this.button)); } disconnectedCallback() { diff --git a/src/components/input/input.ts b/src/components/input/input.ts index dc376807e..b39665163 100644 --- a/src/components/input/input.ts +++ b/src/components/input/input.ts @@ -149,14 +149,9 @@ export default class SlInput extends LitElement { connectedCallback() { super.connectedCallback(); this.handleSlotChange = this.handleSlotChange.bind(this); - this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange); } - firstUpdated() { - this.handleSlotChange(); - } - disconnectedCallback() { super.disconnectedCallback(); this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange); diff --git a/src/components/range/range.ts b/src/components/range/range.ts index 18216cf3d..a4da1015c 100644 --- a/src/components/range/range.ts +++ b/src/components/range/range.ts @@ -85,6 +85,7 @@ export default class SlRange extends LitElement { connectedCallback() { super.connectedCallback(); this.handleSlotChange = this.handleSlotChange.bind(this); + this.resizeObserver = new ResizeObserver(() => this.syncTooltip()); this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange); if (this.value === undefined || this.value === null) this.value = this.min; @@ -92,15 +93,16 @@ export default class SlRange extends LitElement { if (this.value > this.max) this.value = this.max; this.handleSlotChange(); - } - firstUpdated() { - this.syncTooltip(); - this.resizeObserver = new ResizeObserver(() => this.syncTooltip()); + this.updateComplete.then(() => { + this.syncTooltip(); + this.resizeObserver.observe(this.input); + }); } disconnectedCallback() { super.disconnectedCallback(); + this.resizeObserver.unobserve(this.input); this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange); } @@ -131,14 +133,12 @@ export default class SlRange extends LitElement { this.hasFocus = false; this.hasTooltip = false; this.slBlur.emit(); - this.resizeObserver.unobserve(this.input); } handleFocus() { this.hasFocus = true; this.hasTooltip = true; this.slFocus.emit(); - this.resizeObserver.observe(this.input); } @watch('label') diff --git a/src/components/rating/rating.ts b/src/components/rating/rating.ts index b958b74b4..387dc77b9 100644 --- a/src/components/rating/rating.ts +++ b/src/components/rating/rating.ts @@ -62,8 +62,9 @@ export default class SlRating extends LitElement { this.rating.blur(); } - firstUpdated() { - focusVisible.observe(this.rating); + connectedCallback() { + super.connectedCallback(); + this.updateComplete.then(() => focusVisible.observe(this.rating)); } disconnectedCallback() { diff --git a/src/components/relative-time/relative-time.ts b/src/components/relative-time/relative-time.ts index 870020898..a134d60ce 100644 --- a/src/components/relative-time/relative-time.ts +++ b/src/components/relative-time/relative-time.ts @@ -32,7 +32,8 @@ export default class SlRelativeTime extends LitElement { /** Keep the displayed value up to date as time passes. */ @property({ type: Boolean }) sync = false; - firstUpdated() { + connectedCallback() { + super.connectedCallback(); this.updateTime(); } diff --git a/src/components/select/select.ts b/src/components/select/select.ts index 6e7910ec8..0a1109510 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -123,18 +123,18 @@ export default class SlSelect extends LitElement { connectedCallback() { super.connectedCallback(); this.handleSlotChange = this.handleSlotChange.bind(this); - - this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange); - this.handleSlotChange(); - } - - firstUpdated() { this.resizeObserver = new ResizeObserver(() => this.resizeMenu()); - this.syncItemsFromValue(); + + this.updateComplete.then(() => { + this.resizeObserver.observe(this); + this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange); + this.syncItemsFromValue(); + }); } disconnectedCallback() { super.disconnectedCallback(); + this.resizeObserver.unobserve(this); this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange); } @@ -256,12 +256,10 @@ export default class SlSelect extends LitElement { handleMenuShow() { this.resizeMenu(); - this.resizeObserver.observe(this); this.isOpen = true; } handleMenuHide() { - this.resizeObserver.unobserve(this); this.isOpen = false; } diff --git a/src/components/tab-group/tab-group.ts b/src/components/tab-group/tab-group.ts index cc72be733..3240f2867 100644 --- a/src/components/tab-group/tab-group.ts +++ b/src/components/tab-group/tab-group.ts @@ -62,49 +62,49 @@ export default class SlTabGroup extends LitElement { /** 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 - const observer = new IntersectionObserver((entries, observer) => { - if (entries[0].intersectionRatio > 0) { - this.setAriaLabels(); - this.setActiveTab(this.getActiveTab() || this.tabs[0], false); - observer.unobserve(entries[0].target); - } - }); - observer.observe(this); - - focusVisible.observe(this.tabGroup); + connectedCallback() { + super.connectedCallback(); this.resizeObserver = new ResizeObserver(() => { this.preventIndicatorTransition(); this.repositionIndicator(); this.updateScrollControls(); }); - this.resizeObserver.observe(this.nav); - requestAnimationFrame(() => this.updateScrollControls()); this.mutationObserver = new MutationObserver(mutations => { // Update aria labels when the DOM changes - if ( - mutations.some(mutation => !['aria-labelledby', 'aria-controls'].includes(mutation.attributeName as string)) - ) { + if (mutations.some(m => !['aria-labelledby', 'aria-controls'].includes(m.attributeName as string))) { setTimeout(() => this.setAriaLabels()); } // Sync tabs when disabled states change - if (mutations.some(mutation => mutation.attributeName === 'disabled')) { + if (mutations.some(m => m.attributeName === 'disabled')) { this.syncTabsAndPanels(); } }); - this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true }); + + this.updateComplete.then(() => { + this.syncTabsAndPanels(); + this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true }); + this.resizeObserver.observe(this.nav); + focusVisible.observe(this.tabGroup); + + // Set initial tab state when the tabs first become visible + const intersectionObserver = new IntersectionObserver((entries, observer) => { + if (entries[0].intersectionRatio > 0) { + this.setAriaLabels(); + this.setActiveTab(this.getActiveTab() || this.tabs[0], { emitEvents: false }); + observer.unobserve(entries[0].target); + } + }); + intersectionObserver.observe(this.tabGroup); + }); } disconnectedCallback() { this.mutationObserver.disconnect(); - focusVisible.unobserve(this.tabGroup); this.resizeObserver.unobserve(this.nav); + focusVisible.unobserve(this.tabGroup); } /** Shows the specified tab panel. */ @@ -112,7 +112,7 @@ export default class SlTabGroup extends LitElement { const tab = this.tabs.find(el => el.panel === panel) as SlTab; if (tab) { - this.setActiveTab(tab); + this.setActiveTab(tab, { scrollBehavior: 'smooth' }); } } @@ -148,7 +148,7 @@ export default class SlTabGroup extends LitElement { } if (tab) { - this.setActiveTab(tab); + this.setActiveTab(tab, { scrollBehavior: 'smooth' }); } } @@ -165,7 +165,7 @@ export default class SlTabGroup extends LitElement { // Activate a tab if (['Enter', ' '].includes(event.key)) { if (tab) { - this.setActiveTab(tab); + this.setActiveTab(tab, { scrollBehavior: 'smooth' }); event.preventDefault(); } } @@ -190,7 +190,7 @@ export default class SlTabGroup extends LitElement { this.tabs[index].focus({ preventScroll: true }); if (this.activation === 'auto') { - this.setActiveTab(this.tabs[index]); + this.setActiveTab(this.tabs[index], { scrollBehavior: 'smooth' }); } if (['top', 'bottom'].includes(this.placement)) { @@ -226,7 +226,15 @@ export default class SlTabGroup extends LitElement { } } - setActiveTab(tab: SlTab, emitEvents = true) { + setActiveTab(tab: SlTab, options?: { emitEvents?: boolean; scrollBehavior?: 'auto' | 'smooth' }) { + options = Object.assign( + { + emitEvents: true, + scrollBehavior: 'auto' + }, + options + ); + if (tab && tab !== this.activeTab && !tab.disabled) { const previousTab = this.activeTab; this.activeTab = tab; @@ -237,11 +245,11 @@ export default class SlTabGroup extends LitElement { this.syncIndicator(); if (['top', 'bottom'].includes(this.placement)) { - scrollIntoView(this.activeTab, this.nav, 'horizontal'); + scrollIntoView(this.activeTab, this.nav, 'horizontal', options.scrollBehavior); } // Emit events - if (emitEvents) { + if (options.emitEvents) { if (previousTab) { this.slTabHide.emit({ detail: { name: previousTab.panel } }); } diff --git a/src/components/tab-panel/tab-panel.ts b/src/components/tab-panel/tab-panel.ts index 1558639e7..b7a27e680 100644 --- a/src/components/tab-panel/tab-panel.ts +++ b/src/components/tab-panel/tab-panel.ts @@ -24,7 +24,8 @@ export default class SlTabPanel extends LitElement { /** When true, the tab panel will be shown. */ @property({ type: Boolean, reflect: true }) active = false; - firstUpdated() { + connectedCallback() { + super.connectedCallback(); this.id = this.id || this.componentId; } diff --git a/src/components/textarea/textarea.ts b/src/components/textarea/textarea.ts index db62392aa..48e725b25 100644 --- a/src/components/textarea/textarea.ts +++ b/src/components/textarea/textarea.ts @@ -130,15 +130,14 @@ export default class SlTextarea extends LitElement { connectedCallback() { super.connectedCallback(); this.handleSlotChange = this.handleSlotChange.bind(this); - + this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight()); this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange); this.handleSlotChange(); - } - firstUpdated() { - this.setTextareaHeight(); - this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight()); - this.resizeObserver.observe(this.input); + this.updateComplete.then(() => { + this.setTextareaHeight(); + this.resizeObserver.observe(this.input); + }); } disconnectedCallback() { diff --git a/src/components/tooltip/tooltip.ts b/src/components/tooltip/tooltip.ts index 1d3399ffe..e7d2155d2 100644 --- a/src/components/tooltip/tooltip.ts +++ b/src/components/tooltip/tooltip.ts @@ -93,47 +93,46 @@ export default class SlTooltip extends LitElement { connectedCallback() { super.connectedCallback(); - this.handleBlur = this.handleBlur.bind(this); this.handleClick = this.handleClick.bind(this); this.handleFocus = this.handleFocus.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.handleMouseOver = this.handleMouseOver.bind(this); this.handleMouseOut = this.handleMouseOut.bind(this); + + this.updateComplete.then(() => { + this.addEventListener('blur', this.handleBlur, true); + this.addEventListener('focus', this.handleFocus, true); + this.addEventListener('click', this.handleClick); + this.addEventListener('keydown', this.handleKeyDown); + this.addEventListener('mouseover', this.handleMouseOver); + this.addEventListener('mouseout', this.handleMouseOut); + + this.target = this.getTarget(); + this.syncOptions(); + }); } - async firstUpdated() { - this.target = this.getTarget(); - this.syncOptions(); - - this.addEventListener('blur', this.handleBlur, true); - this.addEventListener('focus', this.handleFocus, true); - this.addEventListener('click', this.handleClick); - this.addEventListener('keydown', this.handleKeyDown); - this.addEventListener('mouseover', this.handleMouseOver); - this.addEventListener('mouseout', this.handleMouseOut); - + firstUpdated() { // Set initial visibility this.tooltip.hidden = !this.open; - // Set the initialized flag after the first update is complete - await this.updateComplete; - this.hasInitialized = true; + // Set the initialized flag after the first render is complete + this.updateComplete.then(() => (this.hasInitialized = true)); } disconnectedCallback() { super.disconnectedCallback(); - - if (this.popover) { - this.popover.destroy(); - } - this.removeEventListener('blur', this.handleBlur, true); this.removeEventListener('focus', this.handleFocus, true); this.removeEventListener('click', this.handleClick); this.removeEventListener('keydown', this.handleKeyDown); this.removeEventListener('mouseover', this.handleMouseOver); this.removeEventListener('mouseout', this.handleMouseOut); + + if (this.popover) { + this.popover.destroy(); + } } /** Shows the tooltip. */