Skip to content

Commit

Permalink
feat(core): Hint add mobile behavior (#9547)
Browse files Browse the repository at this point in the history
Co-authored-by: taiga-family-bot <[email protected]>
  • Loading branch information
waterplea and taiga-family-bot authored Oct 21, 2024
1 parent a680e82 commit e650f38
Show file tree
Hide file tree
Showing 14 changed files with 93 additions and 50 deletions.
4 changes: 2 additions & 2 deletions projects/core/animations/animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,9 @@ export const tuiScaleIn = trigger('tuiScaleIn', [
':enter',
[
style({transform: 'scale({{start}})'}),
animate(TRANSITION, style({transform: 'scale({{end}})'})),
animate('{{duration}}ms {{easing}}', style({transform: 'scale({{end}})'})),
],
{params: {end: 1, start: 0, duration: 300}},
{params: {end: 1, start: 0, duration: 300, easing: 'ease-in-out'}},
),
transition(
':leave',
Expand Down
5 changes: 4 additions & 1 deletion projects/core/classes/accessors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export abstract class TuiAccessor {
}

export abstract class TuiPositionAccessor extends TuiAccessor {
public abstract getPosition(rect: DOMRect): TuiPoint;
/*
* TODO @deprecated switching from DOMRect to HTMLElement in v5
*/
public abstract getPosition(rect: DOMRect, element?: HTMLElement): TuiPoint;
}

export abstract class TuiRectAccessor extends TuiAccessor {
Expand Down
14 changes: 10 additions & 4 deletions projects/core/directives/hint/hint-hover.directive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Directive, inject, Input} from '@angular/core';
import {TuiHoveredService} from '@taiga-ui/cdk/directives/hovered';
import {TUI_IS_MOBILE} from '@taiga-ui/cdk/tokens';
import {tuiInjectElement} from '@taiga-ui/cdk/utils/dom';
import {tuiAsDriver, TuiDriver} from '@taiga-ui/core/classes';
import {tuiIsObscured} from '@taiga-ui/core/utils';
Expand All @@ -25,6 +26,7 @@ import {TUI_HINT_OPTIONS} from './hint-options.directive';
exportAs: 'tuiHintHover',
})
export class TuiHintHover extends TuiDriver {
private readonly isMobile = inject(TUI_IS_MOBILE);
private readonly el = tuiInjectElement();
private readonly hovered$ = inject(TuiHoveredService);
private readonly options = inject(TUI_HINT_OPTIONS);
Expand All @@ -33,16 +35,20 @@ export class TuiHintHover extends TuiDriver {
private readonly stream$ = merge(
this.toggle$.pipe(
switchMap((visible) =>
of(visible).pipe(delay(visible ? 0 : this.tuiHintHideDelay)),
this.isMobile
? of(visible)
: of(visible).pipe(delay(visible ? 0 : this.tuiHintHideDelay)),
),
takeUntil(this.hovered$),
repeat(),
),
this.hovered$.pipe(
switchMap((visible) =>
of(visible).pipe(
delay(visible ? this.tuiHintShowDelay : this.tuiHintHideDelay),
),
this.isMobile
? of(visible)
: of(visible).pipe(
delay(visible ? this.tuiHintShowDelay : this.tuiHintHideDelay),
),
),
takeUntil(this.toggle$),
repeat(),
Expand Down
26 changes: 15 additions & 11 deletions projects/core/directives/hint/hint-position.directive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Directive, inject, Input} from '@angular/core';
import {EMPTY_CLIENT_RECT} from '@taiga-ui/cdk/constants';
import {TUI_IS_MOBILE} from '@taiga-ui/cdk/tokens';
import {
tuiFallbackAccessor,
TuiPositionAccessor,
Expand All @@ -12,15 +13,16 @@ import {TuiHintDirective} from './hint.directive';
import type {TuiHintDirection, TuiHintOptions} from './hint-options.directive';
import {TUI_HINT_DIRECTIONS, TUI_HINT_OPTIONS} from './hint-options.directive';

const OFFSET = 8;
const ARROW_OFFSET = 22;
const GAP = 8;
const ARROW_OFFSET = 24;
const TOP = 0;
const LEFT = 1;

@Directive({
standalone: true,
})
export class TuiHintPosition extends TuiPositionAccessor {
private readonly offset = inject(TUI_IS_MOBILE) ? 16 : 8;
private readonly viewport = inject(TUI_VIEWPORT);
private readonly accessor = tuiFallbackAccessor<TuiRectAccessor>('hint')(
inject<any>(TuiRectAccessor),
Expand All @@ -38,34 +40,36 @@ export class TuiHintPosition extends TuiPositionAccessor {

public readonly type = 'hint';

public getPosition({width, height}: DOMRect): TuiPoint {
public getPosition(rect: DOMRect, el?: HTMLElement): TuiPoint {
const width = el?.clientWidth ?? rect.width;
const height = el?.clientHeight ?? rect.height;
const hostRect = this.accessor.getClientRect() ?? EMPTY_CLIENT_RECT;
const leftCenter = hostRect.left + hostRect.width / 2;
const topCenter = hostRect.top + hostRect.height / 2;

this.points['top-left'][TOP] = hostRect.top - height - OFFSET;
this.points['top-left'][TOP] = hostRect.top - height - this.offset;
this.points['top-left'][LEFT] = leftCenter - width + ARROW_OFFSET;
this.points.top[TOP] = this.points['top-left'][TOP];
this.points.top[LEFT] = leftCenter - width / 2;
this.points['top-right'][TOP] = this.points['top-left'][TOP];
this.points['top-right'][LEFT] = leftCenter - ARROW_OFFSET;

this.points['bottom-left'][TOP] = hostRect.bottom + OFFSET;
this.points['bottom-left'][TOP] = hostRect.bottom + this.offset;
this.points['bottom-left'][LEFT] = this.points['top-left'][LEFT];
this.points.bottom[TOP] = this.points['bottom-left'][TOP];
this.points.bottom[LEFT] = this.points.top[LEFT];
this.points['bottom-right'][TOP] = this.points['bottom-left'][TOP];
this.points['bottom-right'][LEFT] = this.points['top-right'][LEFT];

this.points['left-top'][TOP] = topCenter - height + ARROW_OFFSET;
this.points['left-top'][LEFT] = hostRect.left - width - OFFSET;
this.points['left-top'][LEFT] = hostRect.left - width - this.offset;
this.points.left[TOP] = topCenter - height / 2;
this.points.left[LEFT] = this.points['left-top'][LEFT];
this.points['left-bottom'][TOP] = topCenter - ARROW_OFFSET;
this.points['left-bottom'][LEFT] = this.points['left-top'][LEFT];

this.points['right-top'][TOP] = this.points['left-top'][TOP];
this.points['right-top'][LEFT] = hostRect.right + OFFSET;
this.points['right-top'][LEFT] = hostRect.right + this.offset;
this.points.right[TOP] = this.points.left[TOP];
this.points.right[LEFT] = this.points['right-top'][LEFT];
this.points['right-bottom'][TOP] = this.points['left-bottom'][TOP];
Expand Down Expand Up @@ -93,10 +97,10 @@ export class TuiHintPosition extends TuiPositionAccessor {
const viewport = this.viewport.getClientRect();

return (
top > OFFSET / 4 &&
left > OFFSET / 4 &&
top + height < viewport.bottom - OFFSET / 4 &&
left + width < viewport.right - OFFSET / 4
top > GAP &&
left > GAP &&
top + height < viewport.bottom - GAP &&
left + width < viewport.right - GAP
);
}
}
38 changes: 25 additions & 13 deletions projects/core/directives/hint/hint.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core'
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {EMPTY_CLIENT_RECT} from '@taiga-ui/cdk/constants';
import {TuiHoveredService} from '@taiga-ui/cdk/directives/hovered';
import {TUI_IS_MOBILE} from '@taiga-ui/cdk/tokens';
import type {TuiContext} from '@taiga-ui/cdk/types';
import {tuiInjectElement} from '@taiga-ui/cdk/utils/dom';
import {tuiClamp} from '@taiga-ui/cdk/utils/math';
import {tuiPure, tuiPx} from '@taiga-ui/cdk/utils/miscellaneous';
import {tuiFadeIn} from '@taiga-ui/core/animations';
import {tuiFadeIn, tuiScaleIn} from '@taiga-ui/core/animations';
import {
tuiPositionAccessorFor,
TuiRectAccessor,
Expand All @@ -24,7 +25,7 @@ import {TuiHintPointer} from './hint-pointer.directive';
import {TuiHintPosition} from './hint-position.directive';
import {TuiHintUnstyledComponent} from './hint-unstyled.component';

const GAP = 4;
const GAP = 8;

export const TUI_HINT_PROVIDERS = [
TuiPositionService,
Expand All @@ -47,10 +48,12 @@ export const TUI_HINT_PROVIDERS = [
styleUrls: ['./hint.style.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: TUI_HINT_PROVIDERS,
animations: [tuiFadeIn],
animations: [tuiFadeIn, tuiScaleIn],
host: {
'[@tuiScaleIn]': 'isMobile ? options : desktop',
'[@tuiFadeIn]': 'options',
'[class._untouchable]': 'pointer',
'[class._mobile]': 'isMobile',
'[attr.data-appearance]': 'appearance',
'[attr.tuiTheme]': 'appearance',
'(document:click)': 'onClick($event.target)',
Expand All @@ -62,11 +65,16 @@ export class TuiHintComponent<C = any> {
private readonly vvs = inject(TuiVisualViewportService);
private readonly viewport = inject(TUI_VIEWPORT);

protected readonly options = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED));
protected readonly desktop = {value: '', params: {end: 1, start: 1}};
protected readonly options = tuiToAnimationOptions(
inject(TUI_ANIMATIONS_SPEED),
'cubic-bezier(0.35, 1.3, 0.25, 1)',
);

protected readonly pointer = inject(TuiHintPointer, {optional: true});
protected readonly accessor = inject(TuiRectAccessor);

protected readonly hint = injectContext<TuiContext<TuiHintDirective<C>>>().$implicit;
protected readonly isMobile = inject(TUI_IS_MOBILE);

protected readonly content =
this.hint.component.component === TuiHintUnstyledComponent
Expand Down Expand Up @@ -104,23 +112,27 @@ export class TuiHintComponent<C = any> {
}

@tuiPure
private apply(top: string, left: string, beakTop: string, beakLeft: string): void {
private apply(top: string, left: string, beakTop: number, beakLeft: number): void {
this.el.style.top = top;
this.el.style.left = left;
this.el.style.setProperty('--top', beakTop);
this.el.style.setProperty('--left', beakLeft);
this.el.style.setProperty('--top', `${beakTop}%`);
this.el.style.setProperty('--left', `${beakLeft}%`);
this.el.style.setProperty(
'--rotate',
!beakLeft || Math.ceil(beakLeft) === 100 ? '90deg' : '0deg',
);
}

private update(top: number, left: number): void {
const {height, width} = this.el.getBoundingClientRect();
const {clientHeight, clientWidth} = this.el;
const rect = this.accessor.getClientRect();
const viewport = this.viewport.getClientRect();

if (rect === EMPTY_CLIENT_RECT || !height || !width) {
if (rect === EMPTY_CLIENT_RECT || !clientHeight || !clientWidth) {
return;
}

const safeLeft = tuiClamp(left, GAP, viewport.width - width - GAP);
const safeLeft = tuiClamp(left, GAP, viewport.width - clientWidth - GAP);
const [beakTop, beakLeft] = this.vvs.correct([
rect.top + rect.height / 2 - top,
rect.left + rect.width / 2 - safeLeft,
Expand All @@ -129,8 +141,8 @@ export class TuiHintComponent<C = any> {
this.apply(
tuiPx(Math.round(top)),
tuiPx(Math.round(safeLeft)),
tuiPx(Math.round(tuiClamp(beakTop, 1, height - 1))),
tuiPx(Math.round(tuiClamp(beakLeft, 1, width - 1))),
Math.round((tuiClamp(beakTop, 0, clientHeight) / clientHeight) * 100),
Math.round((tuiClamp(beakLeft, 0, clientWidth) / clientWidth) * 100),
);
}
}
18 changes: 14 additions & 4 deletions projects/core/directives/hint/hint.style.less
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,28 @@
white-space: pre-line;
overflow-wrap: break-word;
line-height: 1.25rem;
transform-origin: var(--left) var(--top);

&::before {
content: '';
position: absolute;
top: var(--top);
left: var(--left);
inline-size: 0.5rem;
inline-size: 0.75rem;
block-size: 0.5rem;
border-radius: 0.125rem;
box-sizing: border-box;
background: inherit;
transform: translate(-50%, -50%) rotate(45deg);
mask-image: url('data:image/svg+xml,<svg viewBox="0 0 12 8" xmlns="http://www.w3.org/2000/svg"><path d="M3.61336 1.69607L2.44882 2.96493C1.84795 3.61964 0.949361 3.99951 0.00053941 4C0.000359608 4 0.000179805 4 0 4C0.000179863 4 0.000359764 4 0.000539623 4C0.949362 4.00049 1.84795 4.38036 2.44882 5.03506L3.61336 6.30394C4.55981 7.33517 5.03303 7.85079 5.63254 7.96535C5.87433 8.01155 6.12436 8.01155 6.36616 7.96535C6.96567 7.85079 7.43889 7.33517 8.38534 6.30393L9.54988 5.03507C10.1511 4.37994 11.0505 4 12 4C11.0505 4 10.1511 3.62006 9.54988 2.96493L8.38534 1.69606C7.43889 0.664826 6.96567 0.149207 6.36616 0.0346517C6.12436 -0.0115506 5.87433 -0.0115506 5.63254 0.0346517C5.03303 0.149207 4.55981 0.664827 3.61336 1.69607Z" /></svg>');
transform: translate(-50%, -50%) rotate(var(--rotate));
}

&._mobile {
font: var(--tui-font-text-m);

&::before {
inline-size: 1.5rem;
block-size: 1.125rem;
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18"><path d="M7.22854 3.81615L4.89971 6.6711C3.69732 8.14514 1.8988 9 0 9C1.8988 9 3.69732 9.85486 4.89971 11.3289L7.22854 14.1839L7.22854 14.1839C9.12123 16.5041 10.0676 17.6643 11.2665 17.922C11.75 18.026 12.25 18.026 12.7335 17.922C13.9324 17.6643 14.8788 16.5041 16.7715 14.1839L19.1003 11.3289C20.3027 9.85486 22.1012 9 24 9C22.1012 9 20.3027 8.14514 19.1003 6.6711L16.7715 3.81614C14.8788 1.49586 13.9324 0.335716 12.7335 0.0779663C12.25 -0.0259888 11.75 -0.0259888 11.2665 0.0779663C10.0676 0.335716 9.12123 1.49586 7.22854 3.81614L7.22854 3.81615Z" /></svg>');
}
}

&[data-appearance='error'] {
Expand Down
7 changes: 6 additions & 1 deletion projects/core/services/position.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ export class TuiPositionService extends Observable<TuiPoint> {
animationFrame$
.pipe(
startWith(null),
map(() => this.accessor.getPosition(this.el.getBoundingClientRect())),
map(() =>
this.accessor.getPosition(
this.el.getBoundingClientRect(),
this.el,
),
),
tuiZonefree(zone),
finalize(() => this.accessor.getPosition(EMPTY_CLIENT_RECT)),
)
Expand Down
3 changes: 3 additions & 0 deletions projects/core/styles/components/notification.less
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ tui-notification,
gap: 0.5rem;
padding: 1rem;
font: var(--tui-font-text-m);
line-height: 1.5rem;
border-radius: var(--tui-radius-l);
box-sizing: border-box;
overflow: hidden;
Expand All @@ -49,6 +50,7 @@ tui-notification,
gap: 0.5rem;
padding: 0.375rem 0.625rem;
font: var(--tui-font-text-s);
line-height: 1.25rem;
border-radius: var(--tui-radius-m);

&::before,
Expand Down Expand Up @@ -81,6 +83,7 @@ tui-notification,
gap: 0.375rem;
padding: 0.75rem;
font: var(--tui-font-text-s);
line-height: 1.25rem;
border-radius: var(--tui-radius-m);

&::before,
Expand Down
3 changes: 2 additions & 1 deletion projects/core/utils/miscellaneous/to-animation-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import type {AnimationOptions} from '@angular/animations';

export const TUI_ANIMATIONS_DEFAULT_DURATION = 300;

export function tuiToAnimationOptions(speed: number): AnimationOptions {
export function tuiToAnimationOptions(speed: number, easing?: string): AnimationOptions {
return {
value: '',
params: {
duration: tuiGetDuration(speed),
easing,
},
} as unknown as AnimationOptions;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
Hint
</button>
<tui-icon
tuiHintDescribe="button"
tuiHintDirection="right"
tuiTooltip="Or with external tooltip"
tuiTooltipDescribe="button"
class="tui-space_top-4 tui-space_left-4"
/>
</p>
Expand Down
10 changes: 5 additions & 5 deletions projects/kit/directives/tooltip/tooltip.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class TuiTooltipStyles {}
TuiWithAppearance,
{
directive: TuiHintDescribe,
inputs: ['tuiHintDescribe'],
inputs: ['tuiHintDescribe: tuiTooltipDescribe'],
},
{
directive: TuiHintDirective,
Expand All @@ -63,7 +63,7 @@ class TuiTooltipStyles {}
],
host: {
tuiTooltip: '',
'(mousedown)': 'stopOnMobile($event)',
'(mousedown)': 'onClick($event)',
},
})
export class TuiTooltip implements DoCheck {
Expand All @@ -89,12 +89,12 @@ export class TuiTooltip implements DoCheck {
}
}

protected stopOnMobile(event: MouseEvent): void {
protected onClick(event: MouseEvent): void {
if (this.isMobile) {
event.preventDefault();
event.stopPropagation();
} else {
this.driver.toggle();
}

this.driver.toggle();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {Directive, inject} from '@angular/core';
import {TuiDay, TuiTime} from '@taiga-ui/cdk/date-time';
import {TUI_TEXTFIELD_HOST} from '@taiga-ui/legacy/tokens';

import type {TuiInputDateTimeDirective} from '../input-date-time.directive';
import {TuiInputDateTimeDirective} from '../input-date-time.directive';

@Directive({
standalone: false,
Expand All @@ -18,7 +17,7 @@ import type {TuiInputDateTimeDirective} from '../input-date-time.directive';
},
})
export class TuiNativeDateTimeDirective {
protected readonly host = inject<TuiInputDateTimeDirective>(TUI_TEXTFIELD_HOST);
protected readonly host = inject(TuiInputDateTimeDirective);

protected get value(): string {
if (!this.host.rawValue[0] || !this.host.rawValue[1]) {
Expand Down
Loading

0 comments on commit e650f38

Please sign in to comment.