Skip to content

Add Tooltip component #1621

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 74 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
d48bff7
feat: Added tooltip component
rkaraivanov Mar 18, 2025
94b25ed
feat(tooltip): Add show and hide delay properties with event handling
Zneeky Mar 24, 2025
c3d45f0
feat(tooltip): Add toggle method for showing/hiding tooltip with butt…
Zneeky Mar 24, 2025
c7c4182
feat(tooltip): Add message property for plain text tooltip content
Zneeky Mar 24, 2025
8ae5c7e
feat(tooltip): Enhance accessibility with ARIA attributes for tooltip…
Zneeky Mar 25, 2025
5273de2
feat(tooltip): Refactor show and hide methods to use a delay helper f…
Zneeky Mar 25, 2025
c8584b9
feat(animation): Add scaleInCenter animation
Zneeky Mar 25, 2025
fb87359
fix(tooltip): restore pointerEnter event for tooltip
Zneeky Mar 25, 2025
14f798a
fix(tooltip): correct tooltip visibility state management for fadeOut…
Zneeky Mar 25, 2025
3baadca
feat(tooltip): enhance toggle animation with customizable duration an…
Zneeky Mar 25, 2025
4256569
Merge branch 'master' into rkaraivanov/tooltip
RivaIvanova Mar 26, 2025
b45ba21
test(tooltip): add more tests
RivaIvanova Mar 26, 2025
a95ca8d
fix(tooltip): rename internal flags for clarity and enhance accessibi…
Zneeky Mar 26, 2025
7af8977
Merge branch 'rkaraivanov/tooltip' into aahmedov/add-tooltip-web-comp…
Zneeky Mar 26, 2025
6eab574
feat(tooltip): enhance tooltip parameters and add new properties to s…
Zneeky Mar 26, 2025
699906e
Merge branch 'aahmedov/add-tooltip-web-components' into rkaraivanov/t…
Zneeky Mar 26, 2025
95c36a3
Merge branch 'master' into rkaraivanov/tooltip
RivaIvanova Mar 27, 2025
31245b7
fix(tooltip): simplify event handling and fix tests
Zneeky Mar 27, 2025
785706b
refactor(tooltip): streamline tooltip show/hide logic and remove unus…
Zneeky Mar 27, 2025
fc3c187
Merge branch 'master' into rkaraivanov/tooltip
rkaraivanov Apr 4, 2025
5d6701a
fix(popover): Arrow placement middleware styles
rkaraivanov Apr 4, 2025
81699f3
test(tooltip): add tests for sticky and message properties
RivaIvanova Apr 4, 2025
963d05c
feat(tooltip): add sticky mode for persistent visibility and a close …
Zneeky Apr 4, 2025
366f6a7
Merge branch 'rkaraivanov/tooltip' into aahmedov/tooltip-enhancements
Zneeky Apr 4, 2025
6256340
styles(tooltip): add initial styles for the tooltip
didimmova Apr 4, 2025
bfecb5b
refactor: Popover arrow parts & tooltip service
rkaraivanov Apr 4, 2025
48c105c
Merge remote-tracking branch 'origin/rkaraivanov/tooltip' into rkarai…
rkaraivanov Apr 4, 2025
3a7464b
fix(tooltip): update default offset to 6 and adjust sticky property r…
Zneeky Apr 4, 2025
429c78e
Merge branch 'rkaraivanov/tooltip' into aahmedov/tooltip-enhancements
Zneeky Apr 4, 2025
6849d7a
refactor(tooltip): _applyTooltipStat
Zneeky Apr 4, 2025
a32e691
refactor(tooltip): add comment for escape key handling in hideOnTrig…
Zneeky Apr 4, 2025
e21a021
fix(tooltip): fix toggle method documentation
Zneeky Apr 4, 2025
6fbf5d0
Merge branch 'master' into rkaraivanov/tooltip
RivaIvanova Apr 7, 2025
4f38286
test(tooltip): refactor methods tests
RivaIvanova Apr 7, 2025
838cfbc
Merge branch 'master' into rkaraivanov/tooltip
rkaraivanov Apr 7, 2025
e89175c
refactor: Tooltip event handlers
rkaraivanov Apr 7, 2025
b14bea1
refactor: applyTooltipState logic
rkaraivanov Apr 8, 2025
e52543f
Merge branch 'master' into rkaraivanov/tooltip
RivaIvanova Apr 8, 2025
2bde4f2
feat: Added shift-padding to pass to igc-popover
rkaraivanov Apr 8, 2025
858aafd
Merge branch 'master' into rkaraivanov/tooltip
RivaIvanova Apr 8, 2025
cddd79d
fix(tooltip): fix tooltip interaction handling when the tooltip and t…
Zneeky Apr 8, 2025
5f01f21
Merge branch 'rkaraivanov/tooltip' of https://github.com/IgniteUI/ign…
Zneeky Apr 8, 2025
a04b27c
Merge branch 'master' into rkaraivanov/tooltip
didimmova Apr 9, 2025
41f3ea3
fix: Popover overlapping tooltip anchor
rkaraivanov Apr 9, 2025
76109f3
Merge remote-tracking branch 'origin/rkaraivanov/tooltip' into rkarai…
rkaraivanov Apr 9, 2025
e95c942
Merge branch 'master' into rkaraivanov/tooltip
rkaraivanov Apr 9, 2025
decd408
chore: Resolve conflicts from master
rkaraivanov Apr 10, 2025
004b948
fix(tooltip): role switches to 'status' for sticky tooltips
Zneeky Apr 11, 2025
99f16f9
refactor(tooltip): implement dynamic anchor change for the tooltip
Zneeky Apr 11, 2025
eef12e5
Merge branch 'rkaraivanov/tooltip' of https://github.com/IgniteUI/ign…
Zneeky Apr 11, 2025
c3eb8b6
Merge branch 'master' into rkaraivanov/tooltip
rkaraivanov Apr 14, 2025
2bd9519
feat: Support same trigger for show and hide
rkaraivanov Apr 14, 2025
861c453
test(tooltip): add tests for role switching and transient anchor beha…
Zneeky Apr 14, 2025
b3ec4dd
Merge branch 'rkaraivanov/tooltip' of https://github.com/IgniteUI/ign…
Zneeky Apr 14, 2025
629052f
refactor(tooltip): use physical instead of logical properties for the…
didimmova Apr 14, 2025
740e230
feat(tooltip): align items to center and change igc-icon size for sti…
didimmova Apr 14, 2025
1facddd
Merge branch 'master' into rkaraivanov/tooltip
rkaraivanov Apr 14, 2025
9f31422
feat: Tooltip arrow offset for different placements and RTL mode
rkaraivanov Apr 14, 2025
ad250f4
test(tooltip): add an a11y test for sticky mode and fix test for tran…
Zneeky Apr 14, 2025
b20fe42
fix(tooltip): update default hide-triggers format and enhance show me…
Zneeky Apr 14, 2025
386ae7c
fix(tooltip): fix anchor resolution in hostConnected method
Zneeky Apr 15, 2025
c2ffb2e
Merge branch 'master' into rkaraivanov/tooltip
rkaraivanov Apr 15, 2025
3d166ea
refactor: Tooltip controller
rkaraivanov Apr 15, 2025
cd6ba6d
Merge branch 'master' into rkaraivanov/tooltip
simeonoff Apr 16, 2025
3ed7642
styles(tooltip): refactor styles
didimmova Apr 16, 2025
de0120a
styles(tooltip): use subtitle-2 type style for indigo
didimmova Apr 16, 2025
e9bc714
refactor(tooltip): reset transient anchor to the default anchor value
Zneeky Apr 16, 2025
776dc8d
Merge branch 'rkaraivanov/tooltip' of https://github.com/IgniteUI/ign…
Zneeky Apr 16, 2025
f053392
Merge branch 'master' into rkaraivanov/tooltip
rkaraivanov Apr 17, 2025
105d6e7
feat(tooltip): switch content alignment to be flex-start
didimmova Apr 17, 2025
7c027ff
refactor(tooltip): Anchor targets as weak refs
rkaraivanov Apr 17, 2025
717ea52
refactor: Minor code style changes and a CHANGELOG entry
rkaraivanov Apr 22, 2025
5f56eed
Merge branch 'master' into rkaraivanov/tooltip
RivaIvanova Apr 22, 2025
854ceaf
refactor: Dropped inline property
rkaraivanov Apr 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- New File Input Component(`igc-file-input`)
- File Input component
- Exposed more public API type aliases for component property types like `ButtonVariant`, `PickerMode`, `StepperOrientation`, `HorizontalTransitionAnimation` (carousel and horizontal stepper) and more.
- Tooltip component

### Deprecated
- Some event argument types have been renamed for consistency:
Expand Down
9 changes: 9 additions & 0 deletions src/animations/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ class AnimationController implements ReactiveController {
);
}

public async playExclusive(animation: AnimationReferenceMetadata) {
const [_, event] = await Promise.all([
this.stopAll(),
this.play(animation),
]);

return event.type === 'finish';
}

public hostConnected() {}
}

Expand Down
18 changes: 18 additions & 0 deletions src/animations/presets/scale/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { EaseOut } from '../../easings.js';
import { animation } from '../../types.js';

const baseOptions: KeyframeAnimationOptions = {
duration: 350,
easing: EaseOut.Quad,
};

const scaleInCenter = (options = baseOptions) =>
animation(
[
{ transform: 'scale(0)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 },
],
options
);

export { scaleInCenter };
2 changes: 2 additions & 0 deletions src/components/common/definitions/defineAllComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import IgcTextareaComponent from '../../textarea/textarea.js';
import IgcTileManagerComponent from '../../tile-manager/tile-manager.js';
import IgcTileComponent from '../../tile-manager/tile.js';
import IgcToastComponent from '../../toast/toast.js';
import IgcTooltipComponent from '../../tooltip/tooltip.js';
import IgcTreeItemComponent from '../../tree/tree-item.js';
import IgcTreeComponent from '../../tree/tree.js';
import { defineComponents } from './defineComponents.js';
Expand Down Expand Up @@ -136,6 +137,7 @@ const allComponents: IgniteComponent[] = [
IgcTextareaComponent,
IgcTileComponent,
IgcTileManagerComponent,
IgcTooltipComponent,
];

export function defineAllComponents() {
Expand Down
30 changes: 28 additions & 2 deletions src/components/common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,36 @@ export function isString(value: unknown): value is string {
return typeof value === 'string';
}

export function isObject(value: unknown): value is object {
return value != null && typeof value === 'object';
}

export function isEventListenerObject(x: unknown): x is EventListenerObject {
return isObject(x) && 'handleEvent' in x;
}

export function addWeakEventListener(
element: Element,
event: string,
listener: EventListenerOrEventListenerObject,
options?: AddEventListenerOptions | boolean
): void {
const weakRef = new WeakRef(listener);
const wrapped = (evt: Event) => {
const handler = weakRef.deref();

return isEventListenerObject(handler)
? handler.handleEvent(evt)
: handler?.(evt);
};

element.addEventListener(event, wrapped, options);
}

/**
* Returns whether a given collection has at least one member.
* Returns whether a given collection is empty.
*/
export function isEmpty<T, U extends string>(
export function isEmpty<T, U extends object>(
x: ArrayLike<T> | Set<T> | Map<U, T>
): boolean {
return 'length' in x ? x.length < 1 : x.size < 1;
Expand Down
8 changes: 8 additions & 0 deletions src/components/common/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ export function simulatePointerLeave(
);
}

export function simulateFocus(node: Element) {
node.dispatchEvent(new FocusEvent('focus'));
}

export function simulateBlur(node: Element) {
node.dispatchEvent(new FocusEvent('blur'));
}

export function simulatePointerDown(
node: Element,
options?: PointerEventInit,
Expand Down
99 changes: 89 additions & 10 deletions src/components/popover/popover.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import {
type Middleware,
type MiddlewareData,
type Placement,
arrow,
autoUpdate,
computePosition,
flip,
inline,
limitShift,
offset,
shift,
Expand All @@ -14,6 +18,7 @@ import { property, query, queryAssignedElements } from 'lit/decorators.js';
import { watch } from '../common/decorators/watch.js';
import { registerComponent } from '../common/definitions/register.js';
import {
first,
getElementByIdFromRoot,
isEmpty,
isString,
Expand Down Expand Up @@ -59,7 +64,7 @@ export default class IgcPopoverComponent extends LitElement {
private dispose?: ReturnType<typeof autoUpdate>;
private target?: Element;

@query('#container', true)
@query('#container')
private _container!: HTMLElement;

@queryAssignedElements({ slot: 'anchor', flatten: true })
Expand All @@ -72,6 +77,23 @@ export default class IgcPopoverComponent extends LitElement {
@property()
public anchor?: Element | string;

/**
* Element to render as an "arrow" element for the current popover.
*/
@property({ attribute: false })
public arrow: HTMLElement | null = null;

/** Additional offset to apply to the arrow element if enabled. */
@property({ type: Number, attribute: 'arrow-offset' })
public arrowOffset = 0;

/**
* Improves positioning for inline reference elements that span over multiple lines.
* Useful for tooltips or similar components.
*/
@property({ type: Boolean, reflect: true })
public inline = false;

/**
* When enabled this changes the placement of the floating element in order to keep it
* in view along the main axis.
Expand Down Expand Up @@ -110,8 +132,14 @@ export default class IgcPopoverComponent extends LitElement {
@property({ type: Boolean, reflect: true })
public shift = false;

/**
* Virtual padding for the resolved overflow detection offsets in pixels.
*/
@property({ type: Number, attribute: 'shift-padding' })
public shiftPadding = 0;

@watch('anchor')
protected async anchorChange() {
protected anchorChange() {
const newTarget = isString(this.anchor)
? getElementByIdFromRoot(this, this.anchor)
: this.anchor;
Expand All @@ -127,11 +155,15 @@ export default class IgcPopoverComponent extends LitElement {
this.open ? this.show() : this.hide();
}

@watch('arrow', { waitUntilFirstUpdate: true })
@watch('arrowOffset', { waitUntilFirstUpdate: true })
@watch('flip', { waitUntilFirstUpdate: true })
@watch('inline', { waitUntilFirstUpdate: true })
@watch('offset', { waitUntilFirstUpdate: true })
@watch('placement', { waitUntilFirstUpdate: true })
@watch('sameWidth', { waitUntilFirstUpdate: true })
@watch('shift', { waitUntilFirstUpdate: true })
@watch('shiftPadding', { waitUntilFirstUpdate: true })
protected floatingPropChange() {
this._updateState();
}
Expand All @@ -151,7 +183,10 @@ export default class IgcPopoverComponent extends LitElement {
}

protected show() {
if (!this.target) return;
if (!this.target) {
return;
}

this._showPopover();

this.dispose = autoUpdate(
Expand Down Expand Up @@ -187,14 +222,23 @@ export default class IgcPopoverComponent extends LitElement {
middleware.push(offset(this.offset));
}

if (this.inline) {
middleware.push(inline());
}

if (this.shift) {
middleware.push(
shift({
padding: this.shiftPadding,
limiter: limitShift(),
})
);
}

if (this.arrow) {
middleware.push(arrow({ element: this.arrow }));
}

if (this.flip) {
middleware.push(flip());
}
Expand Down Expand Up @@ -222,25 +266,60 @@ export default class IgcPopoverComponent extends LitElement {
}

private async _updatePosition() {
if (!this.open || !this.target) {
if (!(this.open && this.target)) {
return;
}

const { x, y } = await computePosition(this.target, this._container, {
placement: this.placement ?? 'bottom-start',
middleware: this._createMiddleware(),
strategy: 'fixed',
});
const { x, y, middlewareData, placement } = await computePosition(
this.target,
this._container,
{
placement: this.placement ?? 'bottom-start',
middleware: this._createMiddleware(),
strategy: 'fixed',
}
);

Object.assign(this._container.style, {
left: 0,
top: 0,
transform: `translate(${roundByDPR(x)}px,${roundByDPR(y)}px)`,
});

this._positionArrow(placement, middlewareData);
}

private _positionArrow(placement: Placement, data: MiddlewareData) {
if (!(data.arrow && this.arrow)) {
return;
}

const { x, y } = data.arrow;

// The current placement of the popover along the x/y axis
const currentPlacement = first(placement.split('-'));

// The opposite side where the arrow element should render based on the `currentPlacement`
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[currentPlacement]!;

this.arrow.part = currentPlacement;

Object.assign(this.arrow.style, {
left: x != null ? `${roundByDPR(x + this.arrowOffset)}px` : '',
top: y != null ? `${roundByDPR(y + this.arrowOffset)}px` : '',
[staticSide]: '-4px',
});
}

private _anchorSlotChange() {
if (this.anchor || isEmpty(this._anchors)) return;
if (this.anchor || isEmpty(this._anchors)) {
return;
}

this.target = this._anchors[0];
this._updateState();
Expand Down
Loading