diff --git a/src/components/alert/alert/alert.ts b/src/components/alert/alert/alert.ts index ccbfd86b41..4d9f842c84 100644 --- a/src/components/alert/alert/alert.ts +++ b/src/components/alert/alert/alert.ts @@ -1,4 +1,11 @@ -import { type CSSResultGroup, html, LitElement, nothing, type TemplateResult } from 'lit'; +import { + type CSSResultGroup, + html, + LitElement, + nothing, + type PropertyValues, + type TemplateResult, +} from 'lit'; import { customElement, property } from 'lit/decorators.js'; import type { LinkTargetType } from '../../core/base-elements.js'; @@ -99,7 +106,9 @@ export class SbbAlertElement extends SbbIconNameMixin(LitElement) { private _language = new SbbLanguageController(this); - protected override async firstUpdated(): Promise { + protected override async firstUpdated(changedProperties: PropertyValues): Promise { + super.firstUpdated(changedProperties); + this._open(); } diff --git a/src/components/checkbox/checkbox/checkbox.ts b/src/components/checkbox/checkbox/checkbox.ts index aed73d1b25..a84ad9dcf7 100644 --- a/src/components/checkbox/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox/checkbox.ts @@ -158,7 +158,9 @@ export class SbbCheckboxElement extends SbbUpdateSchedulerMixin( } } - protected override firstUpdated(): void { + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + // We need to wait for the selection-panel to be fully initialized this.startUpdate(); setTimeout(() => { diff --git a/src/components/clock/clock.ts b/src/components/clock/clock.ts index 3052b30cd8..e02f23c295 100644 --- a/src/components/clock/clock.ts +++ b/src/components/clock/clock.ts @@ -1,4 +1,4 @@ -import type { CSSResultGroup, TemplateResult } from 'lit'; +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { ref } from 'lit/directives/ref.js'; @@ -264,7 +264,9 @@ export class SbbClockElement extends LitElement { return new Date(); } - protected override async firstUpdated(): Promise { + protected override async firstUpdated(changedProperties: PropertyValues): Promise { + super.firstUpdated(changedProperties); + this._addEventListeners(); if (this._hasDataNow()) { diff --git a/src/components/container/sticky-bar/sticky-bar.ts b/src/components/container/sticky-bar/sticky-bar.ts index b8559bb59f..8f29c36171 100644 --- a/src/components/container/sticky-bar/sticky-bar.ts +++ b/src/components/container/sticky-bar/sticky-bar.ts @@ -1,4 +1,10 @@ -import { type CSSResultGroup, html, LitElement, type TemplateResult } from 'lit'; +import { + type CSSResultGroup, + html, + LitElement, + type PropertyValues, + type TemplateResult, +} from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { hostAttributes } from '../../core/decorators.js'; @@ -44,7 +50,9 @@ export class SbbStickyBarElement extends LitElement { } } - protected override firstUpdated(): void { + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + if (!this._intersector) { this._intersector = this.shadowRoot!.querySelector('.sbb-sticky-bar__intersector')!; this._observer.observe(this._intersector); diff --git a/src/components/datepicker/datepicker-toggle/datepicker-toggle.ts b/src/components/datepicker/datepicker-toggle/datepicker-toggle.ts index faa658d775..8d6c6e7309 100644 --- a/src/components/datepicker/datepicker-toggle/datepicker-toggle.ts +++ b/src/components/datepicker/datepicker-toggle/datepicker-toggle.ts @@ -165,7 +165,9 @@ export class SbbDatepickerToggleElement extends SbbNegativeMixin(LitElement) { return undefined; } - protected override updated(): void { + protected override updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + this._popoverElement.trigger = this._triggerElement; } diff --git a/src/components/expansion-panel/expansion-panel/expansion-panel.ts b/src/components/expansion-panel/expansion-panel/expansion-panel.ts index b7d53fea77..934ace2af6 100644 --- a/src/components/expansion-panel/expansion-panel/expansion-panel.ts +++ b/src/components/expansion-panel/expansion-panel/expansion-panel.ts @@ -131,7 +131,9 @@ export class SbbExpansionPanelElement extends SbbHydrationMixin(LitElement) { this.removeAttribute('data-accordion'); } - protected override firstUpdated(): void { + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this._initialized = true; } diff --git a/src/components/image/image.ts b/src/components/image/image.ts index d35681a423..9a4cadfeb2 100644 --- a/src/components/image/image.ts +++ b/src/components/image/image.ts @@ -15,7 +15,7 @@ import { SbbBreakpointUltraMax, SbbTypoScaleDefault, } from '@sbb-esta/lyne-design-tokens'; -import type { CSSResultGroup, TemplateResult } from 'lit'; +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement, nothing } from 'lit'; import { customElement, eventOptions, property, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -539,7 +539,9 @@ export class SbbImageElement extends LitElement { `; } - protected override updated(): void { + protected override updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + if (!this._captionElement) { return; } diff --git a/src/components/notification/notification.ts b/src/components/notification/notification.ts index a7df676654..692651b449 100644 --- a/src/components/notification/notification.ts +++ b/src/components/notification/notification.ts @@ -1,4 +1,4 @@ -import type { CSSResultGroup, TemplateResult } from 'lit'; +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @@ -141,7 +141,9 @@ export class SbbNotificationElement extends LitElement { super.connectedCallback(); } - protected override async firstUpdated(): Promise { + protected override async firstUpdated(changedProperties: PropertyValues): Promise { + super.firstUpdated(changedProperties); + this._notificationElement = this.shadowRoot?.querySelector( '.sbb-notification__wrapper', ) as HTMLElement; diff --git a/src/components/popover/popover/popover.ts b/src/components/popover/popover/popover.ts index 65f76b0b6f..b579435c3d 100644 --- a/src/components/popover/popover/popover.ts +++ b/src/components/popover/popover/popover.ts @@ -210,7 +210,9 @@ export class SbbPopoverElement extends LitElement { } } - protected override firstUpdated(): void { + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + if (this._hoverTrigger) { this._overlay.addEventListener('mouseenter', () => this._onOverlayMouseEnter()); this._overlay.addEventListener('mouseleave', () => this._onOverlayMouseLeave()); diff --git a/src/components/radio-button/radio-button-group/radio-button-group.ts b/src/components/radio-button/radio-button-group/radio-button-group.ts index c757f705b0..f853109649 100644 --- a/src/components/radio-button/radio-button-group/radio-button-group.ts +++ b/src/components/radio-button/radio-button-group/radio-button-group.ts @@ -172,7 +172,9 @@ export class SbbRadioButtonGroupElement extends SbbDisabledMixin(LitElement) { } } - protected override firstUpdated(): void { + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this._didLoad = true; this._updateRadios(this.value); } diff --git a/src/components/radio-button/radio-button/radio-button.ts b/src/components/radio-button/radio-button/radio-button.ts index 6bb4da5014..4f606c05f6 100644 --- a/src/components/radio-button/radio-button/radio-button.ts +++ b/src/components/radio-button/radio-button/radio-button.ts @@ -224,7 +224,9 @@ export class SbbRadioButtonElement extends SbbUpdateSchedulerMixin(LitElement) { } } - protected override firstUpdated(): void { + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + // We need to wait for the selection-panel to be fully initialized this.startUpdate(); setTimeout(() => { diff --git a/src/components/select/select.ts b/src/components/select/select.ts index 1fe566da4b..0fb68e803f 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -233,7 +233,9 @@ export class SbbSelectElement extends SbbUpdateSchedulerMixin( this._stateChange.emit({ type: 'value', value: newValue }); } - protected override firstUpdated(): void { + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + // Override the default focus behavior this.focus = () => this._triggerElement.focus(); this.blur = () => this._triggerElement.blur(); diff --git a/src/components/selection-panel/selection-panel.ts b/src/components/selection-panel/selection-panel.ts index f54ebfaf54..42958f8443 100644 --- a/src/components/selection-panel/selection-panel.ts +++ b/src/components/selection-panel/selection-panel.ts @@ -122,7 +122,9 @@ export class SbbSelectionPanelElement extends LitElement { } } - protected override firstUpdated(): void { + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this._initialized = true; } diff --git a/src/components/tabs/tab-group/tab-group.ts b/src/components/tabs/tab-group/tab-group.ts index 5a5f961f6d..c081d3d059 100644 --- a/src/components/tabs/tab-group/tab-group.ts +++ b/src/components/tabs/tab-group/tab-group.ts @@ -1,4 +1,4 @@ -import type { CSSResultGroup, TemplateResult } from 'lit'; +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { ref } from 'lit/directives/ref.js'; @@ -143,7 +143,9 @@ export class SbbTabGroupElement extends LitElement { this.toggleAttribute('data-nested', !!this.parentElement?.closest('sbb-tab-group')); } - protected override firstUpdated(): void { + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this._tabs = this._getTabs(); this._tabs.forEach((tab) => this._configure(tab)); this._initSelection(); diff --git a/src/storybook/helpers/spread.ts b/src/storybook/helpers/spread.ts index c9b6365d80..09261266e1 100644 --- a/src/storybook/helpers/spread.ts +++ b/src/storybook/helpers/spread.ts @@ -12,6 +12,7 @@ export class SbbSpreadDirective extends Directive { } public override update(part: Part, [spreadData]: { [key: string]: unknown }[]): void { + super.update(part, [spreadData]); if (this._element !== (part as ElementPart).element) { this._element = (part as ElementPart).element; } diff --git a/tools/eslint/index.ts b/tools/eslint/index.ts index e2e6819ca2..b257f551a1 100644 --- a/tools/eslint/index.ts +++ b/tools/eslint/index.ts @@ -5,6 +5,7 @@ import * as customElementDecoratorPosition from './custom-element-decorator-posi import * as importExtensionRule from './import-extension-rule.js'; import * as useLocalName from './local-name-rule.js'; import * as missingComponentDocumentation from './missing-component-documentation-rule.js'; +import * as needsSuperCall from './needs-super-call-rule.js'; const plugin: Omit, 'processors'> = { meta: { @@ -17,6 +18,7 @@ const plugin: Omit, 'processors'> = { [importExtensionRule.name]: importExtensionRule.rule, [useLocalName.name]: useLocalName.rule, [customElementDecoratorPosition.name]: customElementDecoratorPosition.rule, + [needsSuperCall.name]: needsSuperCall.rule, }, }; diff --git a/tools/eslint/needs-super-call-rule.ts b/tools/eslint/needs-super-call-rule.ts new file mode 100644 index 0000000000..9275634318 --- /dev/null +++ b/tools/eslint/needs-super-call-rule.ts @@ -0,0 +1,88 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { ESLintUtils } from '@typescript-eslint/utils'; + +const createRule = ESLintUtils.RuleCreator( + (name) => + `https://github.com/lyne-design-system/lyne-components/blob/main/tools/eslint/${name}.ts`, +); + +type MessageIds = 'needsSuperCall' | 'needsSuperCallSuggestion'; + +const hasChangedProperties = ['shouldUpdate', 'willUpdate', 'update', 'firstUpdated', 'updated']; + +const methodsToCheckForSuperCall = [ + 'connectedCallback', + 'disconnectedCallback', + 'requestUpdate', + 'performUpdate', + ...hasChangedProperties, +]; + +export const name = 'needs-super-call-rule'; +export const rule: TSESLint.RuleModule = createRule({ + create(context) { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + MethodDefinition(methodDefinitionNode) { + if ( + methodDefinitionNode.key.type === 'Identifier' && + methodsToCheckForSuperCall.includes(methodDefinitionNode.key.name) + ) { + const name = methodDefinitionNode.key.name; + const hasSuperCall = methodDefinitionNode.value.body?.body?.some( + (node: TSESTree.Statement) => { + if ( + node.type === 'ExpressionStatement' && + node.expression.type === 'CallExpression' && + node.expression.callee.type === 'MemberExpression' && + node.expression.callee.property.type === 'Identifier' + ) { + const memberExpression = node.expression.callee; + return ( + (memberExpression.property as TSESTree.Identifier).name === name && + memberExpression.object.type === 'Super' + ); + } + }, + ); + + if (!hasSuperCall) { + context.report({ + node: methodDefinitionNode, + messageId: 'needsSuperCall', + suggest: [ + { + messageId: 'needsSuperCallSuggestion', + data: { + name: name, + params: hasChangedProperties.includes(name) ? 'changedProperties' : '', + }, + fix: (fixer) => + fixer.insertTextBefore( + methodDefinitionNode.value.body!.body![0], + `super.${name}(${hasChangedProperties.includes(name) ? 'changedProperties' : ''});\n\n `, + ), + }, + ], + }); + } + } + }, + }; + }, + name, + meta: { + docs: { + description: 'method needs super call', + recommended: 'recommended', + }, + messages: { + needsSuperCall: 'Method needs super call.', + needsSuperCallSuggestion: 'Insert super call: super.{{ name }}({{ params }});', + }, + type: 'problem', + schema: [], + hasSuggestions: true, + }, + defaultOptions: [], +});