`](/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. */