Files
privacy.sexy/src/presentation/components/Shared/TooltipWrapper.vue
undergroundwires e17744faf0 Bump to TypeScript 5.5 and enable noImplicitAny
This commit upgrades TypeScript from 5.4 to 5.5 and enables the
`noImplicitAny` option for stricter type checking. It refactors code to
comply with `noImplicitAny` and adapts to new TypeScript features and
limitations.

Key changes:

- Migrate from TypeScript 5.4 to 5.5
- Enable `noImplicitAny` for stricter type checking
- Refactor code to comply with new TypeScript features and limitations

Other supporting changes:

- Refactor progress bar handling for type safety
- Drop 'I' prefix from interfaces to align with new code convention
- Update TypeScript target from `ES2017` and `ES2018`.
  This allows named capturing groups. Otherwise, new TypeScript compiler
  does not compile the project and shows the following error:
  ```
  ...
  TimestampedFilenameGenerator.spec.ts:105:23 - error TS1503: Named capturing groups are only available when targeting 'ES2018' or later
  const pattern = /^(?<timestamp>\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})-(?<scriptName>[^.]+?)(?:\.(?<extension>[^.]+))?$/;// timestamp-scriptName.extension
  ...
  ```
- Refactor usage of `electron-progressbar` for type safety and
  less complexity.
2024-09-26 16:07:37 +02:00

279 lines
8.7 KiB
Vue

<template>
<div class="tooltip">
<!--
Both trigger and tooltip elements are grouped within a single parent for accurate positioning.
It allows the tooltip content to calculate its position based on the trigger's location.
-->
<div
ref="triggeringElement"
class="tooltip__trigger"
>
<slot />
</div>
<div class="tooltip__overlay">
<div
ref="tooltipDisplayElement"
class="tooltip__display"
:style="displayStyles"
>
<div class="tooltip__content">
<slot name="tooltip" />
</div>
<div
ref="arrowElement"
class="tooltip__arrow"
:style="arrowStyles"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import {
useFloating, arrow, shift, flip, type Placement, offset, type Side, type Coords, autoUpdate,
} from '@floating-ui/vue';
import { defineComponent, shallowRef, computed } from 'vue';
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/Resize/UseResizeObserverPolyfill';
import { throttle } from '@/application/Common/Timing/Throttle';
import { type TargetEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener';
import { injectKey } from '@/presentation/injectionSymbols';
import type { CSSProperties } from 'vue';
const GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX = 2;
const ARROW_SIZE_IN_PX = 4;
const DEFAULT_PLACEMENT: Placement = 'top';
export default defineComponent({
setup() {
const tooltipDisplayElement = shallowRef<HTMLElement | undefined>();
const triggeringElement = shallowRef<HTMLElement | undefined>();
const arrowElement = shallowRef<HTMLElement | undefined>();
const eventListener = injectKey((keys) => keys.useAutoUnsubscribedEventListener);
useResizeObserverPolyfill();
const {
floatingStyles, middlewareData, placement, update,
} = useFloating(
triggeringElement,
tooltipDisplayElement,
{
placement: DEFAULT_PLACEMENT,
middleware: [
offset(ARROW_SIZE_IN_PX + GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX),
/* Shifts the element along the specified axes in order to keep it in view. */
shift(),
/* Changes the placement of the floating element in order to keep it in view,
with the ability to flip to any placement. */
flip(),
arrow({ element: arrowElement }),
],
whileElementsMounted: autoUpdate,
},
);
/*
Not using `float-ui`'s `autoUpdate` with `animationFrame: true` because it updates tooltips on
every frame through `requestAnimationFrame`. This behavior is analogous to a continuous loop
(often 60 updates per second and more depending on the refresh rate), which can be excessively
performance-intensive. It's overkill for the application needs and a monkey solution due to
its brute-force nature.
*/
setupTransitionEndEvents(throttle(() => {
update();
}, 400, { excludeLeadingCall: true }), eventListener);
const arrowStyles = computed<CSSProperties>(() => {
if (!middlewareData.value.arrow) {
return {
display: 'none',
};
}
return {
...getArrowPositionStyles(middlewareData.value.arrow, placement.value),
...getArrowAppearanceStyles(),
};
});
return {
tooltipDisplayElement,
triggeringElement,
displayStyles: floatingStyles,
arrowStyles,
arrowElement,
placement,
};
},
});
function getArrowAppearanceStyles(): CSSProperties {
return {
width: `${ARROW_SIZE_IN_PX * 2}px`,
height: `${ARROW_SIZE_IN_PX * 2}px`,
rotate: '45deg',
};
}
function getArrowPositionStyles(
coordinations: Partial<Coords>,
placement: Placement,
): CSSProperties {
const { x, y } = coordinations; // either X or Y is calculated
const oppositeSide = getCounterpartBoxOffsetProperty(placement);
const newStyle: CSSProperties = {
[oppositeSide]: `-${ARROW_SIZE_IN_PX}px`,
position: 'absolute',
left: x ? `${x}px` : undefined,
top: y ? `${y}px` : undefined,
};
return newStyle;
}
function getCounterpartBoxOffsetProperty(placement: Placement): keyof CSSProperties {
const sideCounterparts: Record<Side, keyof CSSProperties> = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
};
const currentSide = placement.split('-')[0] as Side;
return sideCounterparts[currentSide];
}
function setupTransitionEndEvents(
handler: () => void,
listener: TargetEventListener,
) {
const transitionEndEvents: readonly (keyof HTMLElementEventMap)[] = [
'transitionend',
'transitioncancel',
];
transitionEndEvents.forEach((eventName) => {
listener.startListening(document.body, eventName, handler);
});
}
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
$color-tooltip-background: $color-primary-darkest;
.tooltip {
display: inline-flex;
}
@mixin set-visibility($isVisible: true) {
/*
Visibility is controlled through CSS rather than JavaScript. This allows better CSS
consistency by reusing `hover-or-touch` mixin. Using vue directives such as `v-if` and
`v-show` require JavaScript tracking of touch/hover without reuse of `hover-or-touch`.
The `visibility` property is toggled because:
- Using the `display` property doesn't support smooth transitions (e.g., fading out).
- Keeping invisible tooltips in the DOM is a best practice for accessibility (screen readers).
*/
$animation-duration: 0.5s;
transition: opacity $animation-duration, visibility $animation-duration;
@if $isVisible {
visibility: visible;
opacity: 1;
} @else {
visibility: hidden;
opacity: 0;
}
}
@mixin fixed-fullscreen {
/*
This mixin removes the element from the normal document flow, ensuring that it does not disrupt the layout of other elements,
such as causing unintended screen width expansion on smaller mobile screens.
Setting `top`, `left`, `width` and `height` ensures that, the tooltip is prepared to cover the entire viewport, preventing it from
being cropped or causing overflow issues. `pointer-events: none;` disables capturing all events on page.
Other positioning alternatives considered:
- Moving tooltip off the screen using `left` and `top` properties:
- Causes unintended screen width expansion on smaller mobile screens.
- Causes screen shaking on Chromium browsers.
- `overflow: hidden`:
- It does not work automatic positioning of tooltips.
- `transform: translate(-100vw, -100vh)`:
- Causes screen shaking on Chromium browsers.
*/
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
overflow: hidden;
> * { // Restore styles in children
pointer-events: unset;
overflow: unset;
}
}
.tooltip__overlay {
/*
The z-index is set for both visible and invisible states to ensure it maintains its stacking order
above other elements during transitions. This approach prevents the tooltip from falling behind other
elements during the fade-in and fade-out animations.
*/
z-index: 10;
/*
Reset white-space to the default value to prevent inheriting styles from the trigger element.
This prevents unintentional layout issues or overflow.
*/
white-space: normal;
@include set-visibility(false);
@include fixed-fullscreen;
}
.tooltip__trigger {
@include hover-or-touch {
+ .tooltip__overlay {
@include set-visibility(true);
}
}
}
.tooltip__content {
background: $color-tooltip-background;
color: $color-on-primary;
border-radius: 16px;
padding: $spacing-absolute-large $spacing-absolute-medium;
// Explicitly set font styling for tooltips to prevent inconsistent appearances due to style inheritance from trigger elements.
@include base-font-style;
/*
This margin creates a visual buffer between the tooltip and the edges of the document.
It prevents the tooltip from appearing too close to the edges, ensuring a visually pleasing
and balanced layout.
Avoiding setting vertical margin as it disrupts the arrow rendering.
*/
margin-left: $spacing-absolute-xx-small;
margin-right: $spacing-absolute-xx-small;
// Setting max-width increases readability and consistency reducing overlap and clutter.
@include set-property-ch-value-with-fallback(
$property: max-width,
/*
Research in typography suggests that an optimal line length for text readability is between 50-75 characters per line.
Tooltips should be brief, so aiming for the for the lower end of this range (around 50 characters).
*/
$value-in-ch: 50,
)
}
.tooltip__arrow {
background: $color-tooltip-background;
}
</style>