From 8fcb895e578944821aed3876c90bedd6b4eb2e69 Mon Sep 17 00:00:00 2001 From: Alena Ianova Date: Mon, 22 Jun 2026 14:51:14 +0200 Subject: [PATCH 1/6] feat(core): cat-select added tooltips for truncated options on hover --- .../components/cat-select/cat-select.spec.tsx | 163 ++++++++++++++++++ core/src/components/cat-select/cat-select.tsx | 137 ++++++++++++--- 2 files changed, 275 insertions(+), 25 deletions(-) diff --git a/core/src/components/cat-select/cat-select.spec.tsx b/core/src/components/cat-select/cat-select.spec.tsx index 30b96da3..fafdc515 100644 --- a/core/src/components/cat-select/cat-select.spec.tsx +++ b/core/src/components/cat-select/cat-select.spec.tsx @@ -240,4 +240,167 @@ describe('cat-select', () => { expect(updatedOptions[0].render.description).toBe('Updated description'); }); }); + + describe('truncation tooltips', () => { + describe('option label truncation', () => { + it('should mark an option as truncated when scrollHeight exceeds clientHeight', async () => { + const { root, waitForChanges, instance } = await render(); + await instance.connect(stringArrayConnector(['option1', 'option2'])); + + const trigger = root?.shadowRoot?.querySelector('.select-wrapper'); + trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await waitForChanges(); + + const labelEl = root?.shadowRoot?.querySelector('.select-option-label[data-option-key="option1"]'); + if (labelEl) { + Object.defineProperty(labelEl, 'scrollHeight', { value: 50, configurable: true }); + Object.defineProperty(labelEl, 'clientHeight', { value: 24, configurable: true }); + } + + instance['checkOptionsTruncation'](); + await waitForChanges(); + + expect(instance['truncatedOptionKeys'].has('option1')).toBe(true); + expect(instance['truncatedOptionKeys'].has('option2')).toBe(false); + }); + + it('should not mark an option as truncated when scrollHeight equals clientHeight', async () => { + const { root, waitForChanges, instance } = await render(); + await instance.connect(stringArrayConnector(['option1'])); + + const trigger = root?.shadowRoot?.querySelector('.select-wrapper'); + trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await waitForChanges(); + + const labelEl = root?.shadowRoot?.querySelector('.select-option-label[data-option-key="option1"]'); + if (labelEl) { + Object.defineProperty(labelEl, 'scrollHeight', { value: 24, configurable: true }); + Object.defineProperty(labelEl, 'clientHeight', { value: 24, configurable: true }); + } + + instance['checkOptionsTruncation'](); + await waitForChanges(); + + expect(instance['truncatedOptionKeys'].has('option1')).toBe(false); + }); + + it('should clear truncatedOptionKeys when dropdown closes', async () => { + const { root, waitForChanges, instance } = await render(); + await instance.connect(stringArrayConnector(['option1'])); + + instance['truncatedOptionKeys'] = new Set(['option1']); + + const trigger = root?.shadowRoot?.querySelector('.select-wrapper'); + trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await waitForChanges(); + trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await waitForChanges(); + + expect(instance['truncatedOptionKeys'].size).toBe(0); + }); + }); + + describe('pill label truncation', () => { + it('should mark a pill as truncated when scrollWidth exceeds clientWidth', async () => { + const { root, waitForChanges, instance } = await render(); + await instance.connect(stringArrayConnector(['option1', 'option2'])); + + instance['patchState']({ + selection: [ + { item: { id: 'option1' }, render: { label: 'option1' } }, + { item: { id: 'option2' }, render: { label: 'option2' } } + ], + tempSelection: [] + }); + await waitForChanges(); + + const pillSpan = root?.shadowRoot?.querySelector('span[data-pill-index="0"]'); + if (pillSpan) { + Object.defineProperty(pillSpan, 'scrollWidth', { value: 100, configurable: true }); + Object.defineProperty(pillSpan, 'clientWidth', { value: 60, configurable: true }); + } + + instance['checkInputTruncation'](); + await waitForChanges(); + + expect(instance['truncatedPillIndices'].has(0)).toBe(true); + expect(instance['truncatedPillIndices'].has(1)).toBe(false); + }); + + it('should not mark a pill as truncated when scrollWidth equals clientWidth', async () => { + const { root, waitForChanges, instance } = await render(); + await instance.connect(stringArrayConnector(['option1'])); + + instance['patchState']({ + selection: [{ item: { id: 'option1' }, render: { label: 'option1' } }], + tempSelection: [] + }); + await waitForChanges(); + + const pillSpan = root?.shadowRoot?.querySelector('span[data-pill-index="0"]'); + if (pillSpan) { + Object.defineProperty(pillSpan, 'scrollWidth', { value: 60, configurable: true }); + Object.defineProperty(pillSpan, 'clientWidth', { value: 60, configurable: true }); + } + + instance['checkInputTruncation'](); + await waitForChanges(); + + expect(instance['truncatedPillIndices'].has(0)).toBe(false); + }); + }); + + describe('single-select input truncation', () => { + it('should mark input as truncated when scrollWidth exceeds clientWidth', async () => { + const { root, waitForChanges, instance } = await render(); + await instance.connect(stringArrayConnector(['option1'])); + await waitForChanges(); + + const input = root?.shadowRoot?.querySelector('input.select-input'); + if (input) { + Object.defineProperty(input, 'scrollWidth', { value: 200, configurable: true }); + Object.defineProperty(input, 'clientWidth', { value: 80, configurable: true }); + } + + instance['checkInputTruncation'](); + await waitForChanges(); + + expect(instance['singleValueTruncated']).toBe(true); + }); + + it('should not mark input as truncated when scrollWidth equals clientWidth', async () => { + const { root, waitForChanges, instance } = await render(); + await instance.connect(stringArrayConnector(['option1'])); + await waitForChanges(); + + const input = root?.shadowRoot?.querySelector('input.select-input'); + if (input) { + Object.defineProperty(input, 'scrollWidth', { value: 80, configurable: true }); + Object.defineProperty(input, 'clientWidth', { value: 80, configurable: true }); + } + + instance['checkInputTruncation'](); + await waitForChanges(); + + expect(instance['singleValueTruncated']).toBe(false); + }); + + it('should not set singleValueTruncated in multiple mode', async () => { + const { root, waitForChanges, instance } = await render(); + await instance.connect(stringArrayConnector(['option1'])); + await waitForChanges(); + + const input = root?.shadowRoot?.querySelector('input.select-input'); + if (input) { + Object.defineProperty(input, 'scrollWidth', { value: 200, configurable: true }); + Object.defineProperty(input, 'clientWidth', { value: 80, configurable: true }); + } + + instance['checkInputTruncation'](); + await waitForChanges(); + + expect(instance['singleValueTruncated']).toBe(false); + }); + }); + }); }); diff --git a/core/src/components/cat-select/cat-select.tsx b/core/src/components/cat-select/cat-select.tsx index 173aadbe..e2a3bc84 100644 --- a/core/src/components/cat-select/cat-select.tsx +++ b/core/src/components/cat-select/cat-select.tsx @@ -137,6 +137,8 @@ export class CatSelect { private more$: Subject = new Subject(); private valueChangedBySelection = false; private cleanupFloatingUi?: () => void; + private needsOptionsTruncationCheck = false; + private needsInputTruncationCheck = false; @Element() hostElement!: HTMLElement; @@ -150,6 +152,12 @@ export class CatSelect { @State() errorMap?: ErrorMap | true; + @State() truncatedOptionKeys = new Set(); + + @State() truncatedPillIndices = new Set(); + + @State() singleValueTruncated = false; + /** * Whether the label need a marker to shown if the select is required or optional. */ @@ -316,6 +324,12 @@ export class CatSelect { const changed = (key: keyof CatSelectState) => newState[key] !== oldState[key]; if (changed('isOpen')) { this.update(); + if (!newState.isOpen) { + this.truncatedOptionKeys = new Set(); + } + } + if (changed('options') && newState.isOpen) { + this.needsOptionsTruncationCheck = true; } if (changed('activeOptionIndex') && this.state.activeOptionIndex >= 0) { this.dropdown @@ -376,10 +390,28 @@ export class CatSelect { */ @Event() catBlur!: EventEmitter; + componentDidRender(): void { + if (this.needsOptionsTruncationCheck) { + this.needsOptionsTruncationCheck = false; + requestAnimationFrame(() => this.checkOptionsTruncation()); + } + if (this.needsInputTruncationCheck) { + this.needsInputTruncationCheck = false; + this.checkInputTruncation(); + } + } + componentDidLoad(): void { if (this.input) { autosizeInput(this.input, { minWidth: true }); } + this.checkInputTruncation(); + new ResizeObserver(() => { + this.checkInputTruncation(); + if (this.state.isOpen) { + this.checkOptionsTruncation(); + } + }).observe(this.hostElement); } componentWillLoad(): void { @@ -658,7 +690,13 @@ export class CatSelect { aria-orientation="horizontal" class="select-pills" > - {this.state.selection.map((item, i) => ( + {this.state.selection.map((item, i) => ( + ) : null} - {item.render.label} + {item.render.label} {!this.disabled && ( )} - ))} + + ))} ) : this.state.selection.length && this.state.selection[0].render.avatar ? ( ) : null} - (this.input = el)} - autocomplete={this.autoComplete} - data-1p-ignore={this.autoComplete === 'off' ? '' : undefined} - aria-controls={this.isPillboxActive() ? `select-pillbox-${this.id}` : `select-listbox-${this.id}`} - aria-activedescendant={this.activeDescendant} - aria-invalid={this.invalid ? 'true' : undefined} - aria-describedby={this.hasHint ? this.id + '-hint' : undefined} - aria-autocomplete="list" - onInput={this.onInput.bind(this)} - value={!this.multiple ? this.state.term : undefined} - placeholder={this.placeholder} - disabled={this.disabled || this.state.isResolving} - > + + (this.input = el)} + autocomplete={this.autoComplete} + data-1p-ignore={this.autoComplete === 'off' ? '' : undefined} + aria-controls={this.isPillboxActive() ? `select-pillbox-${this.id}` : `select-listbox-${this.id}`} + aria-activedescendant={this.activeDescendant} + aria-invalid={this.invalid ? 'true' : undefined} + aria-describedby={this.hasHint ? this.id + '-hint' : undefined} + aria-autocomplete="list" + onInput={this.onInput.bind(this)} + value={!this.multiple ? this.state.term : undefined} + placeholder={this.placeholder} + disabled={this.disabled || this.state.isResolving} + > + {this.state.isResolving && } {this.invalid && ( @@ -862,7 +907,15 @@ export class CatSelect { > ) : null} - {getLabel()} + + + {getLabel()} + + {item.render.description} @@ -888,7 +941,15 @@ export class CatSelect { > ) : null} - {getLabel()} + + + {getLabel()} + + {item.render.description} @@ -927,6 +988,7 @@ export class CatSelect { this.patchState({ isResolving: false, selection, term, activeOptionIndex: -1 }); this.term$.next(term); this.input && (this.input.value = term); + this.needsInputTruncationCheck = true; }); } @@ -989,6 +1051,7 @@ export class CatSelect { } } this.setTransparentCaret(); + this.needsInputTruncationCheck = true; } private deselect(id: string) { @@ -998,6 +1061,7 @@ export class CatSelect { activeSelectionIndex: -1 }); } + this.needsInputTruncationCheck = true; } private rerenderOptions() { @@ -1089,6 +1153,29 @@ export class CatSelect { this.state = { ...this.state, ...update }; } + private checkOptionsTruncation(): void { + const keys = new Set(); + this.dropdown?.querySelectorAll('.select-option-label[data-option-key]').forEach(el => { + if (el.scrollHeight > el.clientHeight) { + keys.add(el.dataset.optionKey!); + } + }); + this.truncatedOptionKeys = keys; + } + + private checkInputTruncation(): void { + const indices = new Set(); + this.trigger?.querySelectorAll('span[data-pill-index]').forEach(el => { + if (el.scrollWidth > el.clientWidth) { + indices.add(Number(el.dataset.pillIndex)); + } + }); + this.truncatedPillIndices = indices; + if (!this.multiple && this.input) { + this.singleValueTruncated = this.input.scrollWidth > this.input.clientWidth; + } + } + private isPillboxActive() { return this.state.activeSelectionIndex >= 0; } From 97aada1d2dba4e0c763f5ee845e8fba0fbd61869 Mon Sep 17 00:00:00 2001 From: Alena Ianova Date: Tue, 23 Jun 2026 15:10:23 +0200 Subject: [PATCH 2/6] feat(core): cat-select fixed selected item checkbox checked state --- core/src/components/cat-select/cat-select.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/components/cat-select/cat-select.tsx b/core/src/components/cat-select/cat-select.tsx index e2a3bc84..33163040 100644 --- a/core/src/components/cat-select/cat-select.tsx +++ b/core/src/components/cat-select/cat-select.tsx @@ -397,7 +397,7 @@ export class CatSelect { } if (this.needsInputTruncationCheck) { this.needsInputTruncationCheck = false; - this.checkInputTruncation(); + requestAnimationFrame(() => this.checkInputTruncation()); } } @@ -405,7 +405,7 @@ export class CatSelect { if (this.input) { autosizeInput(this.input, { minWidth: true }); } - this.checkInputTruncation(); + requestAnimationFrame(() => this.checkInputTruncation()); new ResizeObserver(() => { this.checkInputTruncation(); if (this.state.isOpen) { @@ -883,6 +883,7 @@ export class CatSelect { class="select-option" id={`select-${this.id}-option-${i}`} aria-selected={isOptionSelected ? 'true' : 'false'} + key={item.item.id} > {this.multiple ? ( Date: Thu, 25 Jun 2026 12:44:52 +0200 Subject: [PATCH 3/6] feat(core): removed tooltip from cat-select on focus --- core/src/components.d.ts | 11 +++ core/src/components/cat-select-demo/readme.md | 1 + core/src/components/cat-select/cat-select.tsx | 1 + core/src/components/cat-select/readme.md | 2 + .../cat-tooltip/cat-tooltip.spec.tsx | 97 ++++++++++++++++++- .../components/cat-tooltip/cat-tooltip.tsx | 56 +++++++---- core/src/components/cat-tooltip/readme.md | 13 +++ 7 files changed, 160 insertions(+), 21 deletions(-) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 07946a81..c7c49227 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2179,6 +2179,11 @@ export namespace Components { * @default 'm' */ "size": 's' | 'm' | 'l'; + /** + * The trigger event(s) that show and hide the tooltip. - 'hover-focus': show on both mouse hover and keyboard focus (default) - 'hover': show on mouse hover only - 'focus': show on keyboard focus only + * @default 'hover-focus' + */ + "trigger": 'hover' | 'focus' | 'hover-focus'; } } export interface CatButtonCustomEvent extends CustomEvent { @@ -5080,6 +5085,11 @@ declare namespace LocalJSX { * @default 'm' */ "size"?: 's' | 'm' | 'l'; + /** + * The trigger event(s) that show and hide the tooltip. - 'hover-focus': show on both mouse hover and keyboard focus (default) - 'hover': show on mouse hover only - 'focus': show on keyboard focus only + * @default 'hover-focus' + */ + "trigger"?: 'hover' | 'focus' | 'hover-focus'; } interface CatAlertAttributes { @@ -5513,6 +5523,7 @@ declare namespace LocalJSX { "showDelay": number; "hideDelay": number; "longTouchDuration": number; + "trigger": 'hover' | 'focus' | 'hover-focus'; } interface IntrinsicElements { diff --git a/core/src/components/cat-select-demo/readme.md b/core/src/components/cat-select-demo/readme.md index 17a5cbab..d5dff87f 100644 --- a/core/src/components/cat-select-demo/readme.md +++ b/core/src/components/cat-select-demo/readme.md @@ -19,6 +19,7 @@ graph TD; cat-select-demo --> cat-select cat-select-demo --> cat-dropdown cat-select-demo --> cat-button + cat-select --> cat-tooltip cat-select --> cat-avatar cat-select --> cat-button cat-select --> cat-spinner diff --git a/core/src/components/cat-select/cat-select.tsx b/core/src/components/cat-select/cat-select.tsx index 33163040..d172ff0e 100644 --- a/core/src/components/cat-select/cat-select.tsx +++ b/core/src/components/cat-select/cat-select.tsx @@ -743,6 +743,7 @@ export class CatSelect { > ) : null} ` ### Depends on +- [cat-tooltip](../cat-tooltip) - [cat-avatar](../cat-avatar) - [cat-button](../cat-button) - [cat-spinner](../cat-spinner) @@ -142,6 +143,7 @@ Type: `Promise` ### Graph ```mermaid graph TD; + cat-select --> cat-tooltip cat-select --> cat-avatar cat-select --> cat-button cat-select --> cat-spinner diff --git a/core/src/components/cat-tooltip/cat-tooltip.spec.tsx b/core/src/components/cat-tooltip/cat-tooltip.spec.tsx index 1bfe18df..a6acd772 100644 --- a/core/src/components/cat-tooltip/cat-tooltip.spec.tsx +++ b/core/src/components/cat-tooltip/cat-tooltip.spec.tsx @@ -1,9 +1,22 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render } from '@stencil/vitest'; import { h } from '@stencil/core'; + +vi.mock('@floating-ui/dom', () => ({ + autoUpdate: vi.fn(() => vi.fn()), + computePosition: vi.fn(() => Promise.resolve({ x: 0, y: 0 })), + flip: vi.fn(() => ({})), + offset: vi.fn(() => ({})), + shift: vi.fn(() => ({})) +})); + import './cat-tooltip'; describe('cat-tooltip', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('renders', async () => { const { root } = await render( @@ -18,4 +31,86 @@ describe('cat-tooltip', () => { `); }); + + describe('trigger prop', () => { + const waitForTimer = () => new Promise(resolve => setTimeout(resolve, 10)); + + describe('default (hover-focus)', () => { + it('should show on mouseenter', async () => { + const { root, waitForChanges, instance } = await render( + +

Trigger

+
+ ); + root.querySelector('p')!.dispatchEvent(new MouseEvent('mouseenter')); + await waitForTimer(); + await waitForChanges(); + expect(instance.open).toBe(true); + }); + + it('should show on focusin', async () => { + const { root, waitForChanges, instance } = await render( + +

Trigger

+
+ ); + root.querySelector('p')!.dispatchEvent(new FocusEvent('focusin')); + await waitForTimer(); + await waitForChanges(); + expect(instance.open).toBe(true); + }); + }); + + describe('trigger="hover"', () => { + it('should show on mouseenter', async () => { + const { root, waitForChanges, instance } = await render( + +

Trigger

+
+ ); + root.querySelector('p')!.dispatchEvent(new MouseEvent('mouseenter')); + await waitForTimer(); + await waitForChanges(); + expect(instance.open).toBe(true); + }); + + it('should not show on focusin', async () => { + const { root, waitForChanges, instance } = await render( + +

Trigger

+
+ ); + root.querySelector('p')!.dispatchEvent(new FocusEvent('focusin')); + await waitForTimer(); + await waitForChanges(); + expect(instance.open).toBe(false); + }); + }); + + describe('trigger="focus"', () => { + it('should show on focusin', async () => { + const { root, waitForChanges, instance } = await render( + +

Trigger

+
+ ); + root.querySelector('p')!.dispatchEvent(new FocusEvent('focusin')); + await waitForTimer(); + await waitForChanges(); + expect(instance.open).toBe(true); + }); + + it('should not show on mouseenter', async () => { + const { root, waitForChanges, instance } = await render( + +

Trigger

+
+ ); + root.querySelector('p')!.dispatchEvent(new MouseEvent('mouseenter')); + await waitForTimer(); + await waitForChanges(); + expect(instance.open).toBe(false); + }); + }); + }); }); diff --git a/core/src/components/cat-tooltip/cat-tooltip.tsx b/core/src/components/cat-tooltip/cat-tooltip.tsx index 9247b2e8..d2b7f764 100644 --- a/core/src/components/cat-tooltip/cat-tooltip.tsx +++ b/core/src/components/cat-tooltip/cat-tooltip.tsx @@ -22,7 +22,7 @@ export class CatTooltip { private static readonly SHIFT_PADDING = 4; private readonly id = `cat-tooltip-${nextUniqueId++}`; private tooltip?: HTMLElement; - private trigger?: Element; + private triggerEl?: Element; private showTimeout?: number; private hideTimeout?: number; private touchTimeout?: number; @@ -88,6 +88,14 @@ export class CatTooltip { */ @Prop() longTouchDuration = 1000; + /** + * The trigger event(s) that show and hide the tooltip. + * - 'hover-focus': show on both mouse hover and keyboard focus (default) + * - 'hover': show on mouse hover only + * - 'focus': show on keyboard focus only + */ + @Prop() trigger: 'hover' | 'focus' | 'hover-focus' = 'hover-focus'; + @Listen('keydown', { target: 'window' }) handleKeyDown({ key }: KeyboardEvent) { key === 'Escape' && this.hideTooltip(); @@ -95,9 +103,9 @@ export class CatTooltip { componentDidLoad(): void { const slot = this.hostElement.shadowRoot?.querySelector('slot'); - this.trigger = slot?.assignedElements?.()?.[0]; - if (this.trigger && !this.trigger.hasAttribute('aria-describedby')) { - this.trigger.setAttribute('aria-describedby', this.id); + this.triggerEl = slot?.assignedElements?.()?.[0]; + if (this.triggerEl && !this.triggerEl.hasAttribute('aria-describedby')) { + this.triggerEl.setAttribute('aria-describedby', this.id); } this.addListeners(); @@ -143,34 +151,42 @@ export class CatTooltip { } private addListeners() { - this.trigger?.addEventListener('focusin', this.boundShowListener); - this.trigger?.addEventListener('focusout', this.boundHideListener); - this.trigger?.addEventListener('mouseenter', this.boundShowListener); - this.trigger?.addEventListener('mouseleave', this.boundHideListener); + if (this.trigger !== 'hover') { + this.triggerEl?.addEventListener('focusin', this.boundShowListener); + this.triggerEl?.addEventListener('focusout', this.boundHideListener); + } + if (this.trigger !== 'focus') { + this.triggerEl?.addEventListener('mouseenter', this.boundShowListener); + this.triggerEl?.addEventListener('mouseleave', this.boundHideListener); + } if (isTouchScreen) { window.addEventListener('touchstart', this.boundWindowTouchStartListener); - this.trigger?.addEventListener('touchstart', this.boundTouchStartListener); - this.trigger?.addEventListener('touchend', this.boundTouchEndListener); + this.triggerEl?.addEventListener('touchstart', this.boundTouchStartListener); + this.triggerEl?.addEventListener('touchend', this.boundTouchEndListener); } } private removeListeners() { - this.trigger?.removeEventListener('mouseenter', this.boundShowListener); - this.trigger?.removeEventListener('mouseleave', this.boundHideListener); - this.trigger?.removeEventListener('focusin', this.boundShowListener); - this.trigger?.removeEventListener('focusout', this.boundHideListener); + if (this.trigger !== 'hover') { + this.triggerEl?.removeEventListener('focusin', this.boundShowListener); + this.triggerEl?.removeEventListener('focusout', this.boundHideListener); + } + if (this.trigger !== 'focus') { + this.triggerEl?.removeEventListener('mouseenter', this.boundShowListener); + this.triggerEl?.removeEventListener('mouseleave', this.boundHideListener); + } if (isTouchScreen) { window.removeEventListener('touchstart', this.boundWindowTouchStartListener); - this.trigger?.removeEventListener('touchstart', this.boundTouchStartListener); - this.trigger?.removeEventListener('touchend', this.boundTouchEndListener); + this.triggerEl?.removeEventListener('touchstart', this.boundTouchStartListener); + this.triggerEl?.removeEventListener('touchend', this.boundTouchEndListener); } } private async update() { - if (this.trigger && this.tooltip) { - await computePosition(this.trigger, this.tooltip, { + if (this.triggerEl && this.tooltip) { + await computePosition(this.triggerEl, this.tooltip, { strategy: 'fixed', placement: this.placement, middleware: [ @@ -233,8 +249,8 @@ export class CatTooltip { private showTooltip() { if (!this.inactive) { - if (this.trigger && this.tooltip) { - this.cleanupFloatingUi = autoUpdate(this.trigger, this.tooltip, () => this.update()); + if (this.triggerEl && this.tooltip) { + this.cleanupFloatingUi = autoUpdate(this.triggerEl, this.tooltip, () => this.update()); } this.open = true; this.tooltip?.classList.add('tooltip-show'); diff --git a/core/src/components/cat-tooltip/readme.md b/core/src/components/cat-tooltip/readme.md index 0b84ce63..cff46dec 100644 --- a/core/src/components/cat-tooltip/readme.md +++ b/core/src/components/cat-tooltip/readme.md @@ -25,6 +25,19 @@ different placements, sizes, and styles. | `size` | `size` | The size of the tooltip. | `"l" \| "m" \| "s"` | `'m'` | +## Dependencies + +### Used by + + - [cat-select](../cat-select) + +### Graph +```mermaid +graph TD; + cat-select --> cat-tooltip + style cat-tooltip fill:#f9f,stroke:#333,stroke-width:4px +``` + ---------------------------------------------- Made with love in Hamburg, Germany From 857ac90ac1888d975d16f43f693506f6997b214d Mon Sep 17 00:00:00 2001 From: Alena Ianova Date: Thu, 25 Jun 2026 13:04:15 +0200 Subject: [PATCH 4/6] feat(core): cr fixes --- .../catalyst/src/lib/directives/proxies.ts | 24 ++++- core/src/components/cat-select/cat-select.tsx | 92 ++++++++++--------- core/src/components/cat-tooltip/readme.md | 21 +++-- 3 files changed, 82 insertions(+), 55 deletions(-) diff --git a/angular/projects/catalyst/src/lib/directives/proxies.ts b/angular/projects/catalyst/src/lib/directives/proxies.ts index 4ccea2c5..9b6c9364 100644 --- a/angular/projects/catalyst/src/lib/directives/proxies.ts +++ b/angular/projects/catalyst/src/lib/directives/proxies.ts @@ -1791,14 +1791,34 @@ export declare interface CatToggle extends Components.CatToggle { } @ProxyCmp({ - inputs: ['content', 'disabled', 'hideDelay', 'longTouchDuration', 'placement', 'round', 'showDelay', 'size'] + inputs: [ + 'content', + 'disabled', + 'hideDelay', + 'longTouchDuration', + 'placement', + 'round', + 'showDelay', + 'size', + 'trigger' + ] }) @Component({ selector: 'cat-tooltip', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['content', 'disabled', 'hideDelay', 'longTouchDuration', 'placement', 'round', 'showDelay', 'size'], + inputs: [ + 'content', + 'disabled', + 'hideDelay', + 'longTouchDuration', + 'placement', + 'round', + 'showDelay', + 'size', + 'trigger' + ], standalone: false }) export class CatTooltip { diff --git a/core/src/components/cat-select/cat-select.tsx b/core/src/components/cat-select/cat-select.tsx index d172ff0e..c0fd2b76 100644 --- a/core/src/components/cat-select/cat-select.tsx +++ b/core/src/components/cat-select/cat-select.tsx @@ -139,6 +139,7 @@ export class CatSelect { private cleanupFloatingUi?: () => void; private needsOptionsTruncationCheck = false; private needsInputTruncationCheck = false; + private resizeObserver?: ResizeObserver; @Element() hostElement!: HTMLElement; @@ -406,12 +407,13 @@ export class CatSelect { autosizeInput(this.input, { minWidth: true }); } requestAnimationFrame(() => this.checkInputTruncation()); - new ResizeObserver(() => { + this.resizeObserver = new ResizeObserver(() => { this.checkInputTruncation(); if (this.state.isOpen) { this.checkOptionsTruncation(); } - }).observe(this.hostElement); + }); + this.resizeObserver.observe(this.hostElement); } componentWillLoad(): void { @@ -423,6 +425,10 @@ export class CatSelect { this.hasSlottedHint = !!this.hostElement.querySelector('[slot="hint"]'); } + disconnectedCallback(): void { + this.resizeObserver?.disconnect(); + } + @Listen('blur') onBlur(event: FocusEvent): void { if (!this.multiple && this.state.activeOptionIndex >= 0) { @@ -690,48 +696,48 @@ export class CatSelect { aria-orientation="horizontal" class="select-pills" > - {this.state.selection.map((item, i) => ( - - ( + - {item.render.avatar ? ( - - ) : null} - {item.render.label} - {!this.disabled && ( - this.deselect(item.item.id)} - tabIndex={-1} - data-dropdown-no-close - > - )} - - - ))} + + {item.render.avatar ? ( + + ) : null} + {item.render.label} + {!this.disabled && ( + this.deselect(item.item.id)} + tabIndex={-1} + data-dropdown-no-close + > + )} + +
+ ))} ) : this.state.selection.length && this.state.selection[0].render.avatar ? ( Date: Thu, 25 Jun 2026 13:24:05 +0200 Subject: [PATCH 5/6] feat(core): cr fixes --- core/src/components/cat-select/cat-select.tsx | 4 ++-- core/src/components/cat-tooltip/cat-tooltip.tsx | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/core/src/components/cat-select/cat-select.tsx b/core/src/components/cat-select/cat-select.tsx index c0fd2b76..867ad8f9 100644 --- a/core/src/components/cat-select/cat-select.tsx +++ b/core/src/components/cat-select/cat-select.tsx @@ -698,7 +698,7 @@ export class CatSelect { > {this.state.selection.map((item, i) => ( - + {getLabel()} diff --git a/core/src/components/cat-tooltip/cat-tooltip.tsx b/core/src/components/cat-tooltip/cat-tooltip.tsx index d2b7f764..a25e4274 100644 --- a/core/src/components/cat-tooltip/cat-tooltip.tsx +++ b/core/src/components/cat-tooltip/cat-tooltip.tsx @@ -168,14 +168,10 @@ export class CatTooltip { } private removeListeners() { - if (this.trigger !== 'hover') { - this.triggerEl?.removeEventListener('focusin', this.boundShowListener); - this.triggerEl?.removeEventListener('focusout', this.boundHideListener); - } - if (this.trigger !== 'focus') { - this.triggerEl?.removeEventListener('mouseenter', this.boundShowListener); - this.triggerEl?.removeEventListener('mouseleave', this.boundHideListener); - } + this.triggerEl?.removeEventListener('focusin', this.boundShowListener); + this.triggerEl?.removeEventListener('focusout', this.boundHideListener); + this.triggerEl?.removeEventListener('mouseenter', this.boundShowListener); + this.triggerEl?.removeEventListener('mouseleave', this.boundHideListener); if (isTouchScreen) { window.removeEventListener('touchstart', this.boundWindowTouchStartListener); From 05fc4de46e03335402a9773846a477c96563f721 Mon Sep 17 00:00:00 2001 From: Alena Ianova Date: Thu, 25 Jun 2026 16:14:53 +0200 Subject: [PATCH 6/6] feat(core): cr fixes --- core/src/components/cat-select/cat-select.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/cat-select/cat-select.tsx b/core/src/components/cat-select/cat-select.tsx index 867ad8f9..19f23188 100644 --- a/core/src/components/cat-select/cat-select.tsx +++ b/core/src/components/cat-select/cat-select.tsx @@ -698,7 +698,6 @@ export class CatSelect { > {this.state.selection.map((item, i) => ( element is a stencil dom directive, it prevents stale checked state when options reload return (