mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-04-24 06:17:08 -04:00
Use floating UI for Tooltip
Fixed: Tooltips cutoff by edge of screen Closes #7705
This commit is contained in:
parent
38cd63ec04
commit
a6e6b7518d
8 changed files with 157 additions and 370 deletions
|
@ -61,7 +61,7 @@ function QueueDetails(props: QueueDetailsProps) {
|
|||
anchor={progressBar!}
|
||||
title={`${state} - ${progress.toFixed(1)}%`}
|
||||
body={<div>{title}</div>}
|
||||
position={tooltipPositions.LEFT}
|
||||
position="bottom-start"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,9 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import useTheme from 'Helpers/Hooks/useTheme';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
|
||||
(theme) => {
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme() {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
const theme = useTheme();
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.tooltipContainer {
|
||||
z-index: $popperZIndex;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
|
@ -18,174 +17,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
.arrow,
|
||||
.arrow::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 11px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.arrowDisabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.arrow::after {
|
||||
border-width: 10px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.top {
|
||||
bottom: -11px;
|
||||
margin-left: -11px;
|
||||
border-bottom-width: 0;
|
||||
|
||||
&::after {
|
||||
bottom: 1px;
|
||||
margin-left: -10px;
|
||||
border-bottom-width: 0;
|
||||
content: ' ';
|
||||
|
||||
&.default {
|
||||
border-top-color: var(--popoverArrowBorderColor);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
border-top-color: var(--popoverArrowBorderInverseColor);
|
||||
}
|
||||
}
|
||||
|
||||
&.default {
|
||||
border-top-color: var(--popoverArrowBorderColor);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
border-top-color: var(--popoverArrowBorderInverseColor);
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
left: -11px;
|
||||
margin-top: -11px;
|
||||
border-left-width: 0;
|
||||
|
||||
&::after {
|
||||
bottom: -10px;
|
||||
left: 1px;
|
||||
border-left-width: 0;
|
||||
content: ' ';
|
||||
|
||||
&.default {
|
||||
border-right-color: var(--popoverArrowBorderColor);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
border-right-color: var(--popoverArrowBorderInverseColor);
|
||||
}
|
||||
}
|
||||
|
||||
&.default {
|
||||
border-right-color: var(--popoverArrowBorderColor);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
border-right-color: var(--popoverArrowBorderInverseColor);
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
top: -11px;
|
||||
margin-left: -11px;
|
||||
border-top-width: 0;
|
||||
|
||||
&::after {
|
||||
top: 1px;
|
||||
margin-left: -10px;
|
||||
border-top-width: 0;
|
||||
content: ' ';
|
||||
|
||||
&.default {
|
||||
border-bottom-color: var(--popoverArrowBorderColor);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
border-bottom-color: var(--popoverArrowBorderInverseColor);
|
||||
}
|
||||
}
|
||||
|
||||
&.default {
|
||||
border-bottom-color: var(--popoverArrowBorderColor);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
border-bottom-color: var(--popoverArrowBorderInverseColor);
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
right: -11px;
|
||||
margin-top: -11px;
|
||||
border-right-width: 0;
|
||||
|
||||
&::after {
|
||||
right: 1px;
|
||||
bottom: -10px;
|
||||
border-right-width: 0;
|
||||
content: ' ';
|
||||
|
||||
&.default {
|
||||
border-left-color: var(--popoverArrowBorderColor);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
border-left-color: var(--popoverArrowBorderInverseColor);
|
||||
}
|
||||
}
|
||||
|
||||
&.default {
|
||||
border-left-color: var(--popoverArrowBorderColor);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
border-left-color: var(--popoverArrowBorderInverseColor);
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.verticalContainer {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointExtraSmall - 20px);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointExtraSmall) {
|
||||
.horizontalContainer {
|
||||
.tooltip {
|
||||
max-width: calc($breakpointSmall * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointSmall) {
|
||||
.horizontalContainer {
|
||||
.tooltip {
|
||||
max-width: calc($breakpointMedium * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointMedium) {
|
||||
.horizontalContainer {
|
||||
.tooltip {
|
||||
max-width: calc($breakpointLarge * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* @media only screen and (max-width: $breakpointLarge) {
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointLarge * 0.8);
|
||||
}
|
||||
} */
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'arrow': string;
|
||||
'arrowDisabled': string;
|
||||
'body': string;
|
||||
'bottom': string;
|
||||
'default': string;
|
||||
'horizontalContainer': string;
|
||||
'inverse': string;
|
||||
'left': string;
|
||||
'right': string;
|
||||
'tooltip': string;
|
||||
'tooltipContainer': string;
|
||||
'top': string;
|
||||
'verticalContainer': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
import {
|
||||
arrow,
|
||||
autoUpdate,
|
||||
flip,
|
||||
FloatingArrow,
|
||||
FloatingPortal,
|
||||
offset,
|
||||
Placement,
|
||||
safePolygon,
|
||||
shift,
|
||||
useClick,
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useHover,
|
||||
useInteractions,
|
||||
} from '@floating-ui/react';
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import Portal from 'Components/Portal';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useThemeColor } from 'Helpers/Hooks/useTheme';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import { isMobile } from 'Utilities/browser';
|
||||
import styles from './Tooltip.css';
|
||||
|
||||
export interface TooltipProps {
|
||||
|
@ -19,8 +27,8 @@ export interface TooltipProps {
|
|||
bodyClassName?: string;
|
||||
anchor: React.ReactNode;
|
||||
tooltip: string | React.ReactNode;
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
position?: (typeof tooltipPositions.all)[number];
|
||||
kind?: Extract<Kind, 'default' | 'inverse'>;
|
||||
position?: Placement;
|
||||
canFlip?: boolean;
|
||||
}
|
||||
function Tooltip(props: TooltipProps) {
|
||||
|
@ -30,196 +38,76 @@ function Tooltip(props: TooltipProps) {
|
|||
anchor,
|
||||
tooltip,
|
||||
kind = kinds.DEFAULT,
|
||||
position = tooltipPositions.TOP,
|
||||
canFlip = false,
|
||||
position,
|
||||
canFlip = true,
|
||||
} = props;
|
||||
|
||||
const closeTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
const updater = useRef<(() => void) | null>(null);
|
||||
const arrowColor = useThemeColor(
|
||||
kind === 'inverse'
|
||||
? 'popoverArrowBorderInverseColor'
|
||||
: 'popoverArrowBorderColor'
|
||||
);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!isMobileUtil()) {
|
||||
return;
|
||||
}
|
||||
const arrowRef = useRef(null);
|
||||
|
||||
setIsOpen((isOpen) => {
|
||||
return !isOpen;
|
||||
});
|
||||
}, [setIsOpen]);
|
||||
|
||||
const handleMouseEnterAnchor = useCallback(() => {
|
||||
// Mobile will fire mouse enter and click events rapidly,
|
||||
// this causes the tooltip not to open on the first press.
|
||||
// Ignore the mouse enter event on mobile.
|
||||
|
||||
if (isMobileUtil()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (closeTimeout.current) {
|
||||
clearTimeout(closeTimeout.current);
|
||||
}
|
||||
|
||||
setIsOpen(true);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const handleMouseEnterTooltip = useCallback(() => {
|
||||
if (closeTimeout.current) {
|
||||
clearTimeout(closeTimeout.current);
|
||||
}
|
||||
|
||||
setIsOpen(true);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
// Still listen for mouse leave on mobile to allow clicks outside to close the tooltip.
|
||||
|
||||
clearTimeout(closeTimeout.current);
|
||||
closeTimeout.current = setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
}, 100);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const maxWidth = useMemo(() => {
|
||||
const windowWidth = window.innerWidth;
|
||||
|
||||
if (windowWidth >= parseInt(dimensions.breakpointLarge)) {
|
||||
return 800;
|
||||
} else if (windowWidth >= parseInt(dimensions.breakpointMedium)) {
|
||||
return 650;
|
||||
} else if (windowWidth >= parseInt(dimensions.breakpointSmall)) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
return 450;
|
||||
}, []);
|
||||
|
||||
const computeMaxSize = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(data: any) => {
|
||||
const { top, right, bottom, left } = data.offsets.reference;
|
||||
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if (/^top/.test(data.placement)) {
|
||||
data.styles.maxHeight = top - 20;
|
||||
} else if (/^bottom/.test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom - 20;
|
||||
} else if (/^right/.test(data.placement)) {
|
||||
data.styles.maxWidth = Math.min(maxWidth, windowWidth - right - 20);
|
||||
data.styles.maxHeight = top - 20;
|
||||
} else {
|
||||
data.styles.maxWidth = Math.min(maxWidth, left - 20);
|
||||
data.styles.maxHeight = top - 20;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
[maxWidth]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (updater.current && isOpen) {
|
||||
updater.current();
|
||||
}
|
||||
const { refs, context, floatingStyles } = useFloating({
|
||||
middleware: [
|
||||
arrow({
|
||||
element: arrowRef,
|
||||
}),
|
||||
flip({
|
||||
crossAxis: canFlip,
|
||||
mainAxis: canFlip,
|
||||
}),
|
||||
offset({ mainAxis: 10 }),
|
||||
shift(),
|
||||
],
|
||||
open: isOpen,
|
||||
placement: position,
|
||||
whileElementsMounted: autoUpdate,
|
||||
onOpenChange: setIsOpen,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (closeTimeout.current) {
|
||||
clearTimeout(closeTimeout.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
const click = useClick(context, {
|
||||
enabled: isMobile(),
|
||||
});
|
||||
const dismiss = useDismiss(context);
|
||||
const hover = useHover(context, {
|
||||
handleClose: safePolygon(),
|
||||
});
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||
click,
|
||||
dismiss,
|
||||
hover,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<span
|
||||
ref={ref}
|
||||
className={className}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnterAnchor}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
<>
|
||||
<span
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
className={className}
|
||||
>
|
||||
{anchor}
|
||||
</span>
|
||||
{isOpen ? (
|
||||
<FloatingPortal id="portal-root">
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className={styles.tooltipContainer}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
{anchor}
|
||||
</span>
|
||||
)}
|
||||
</Reference>
|
||||
|
||||
<Portal>
|
||||
<Popper
|
||||
// @ts-expect-error - PopperJS types are not in sync with our position types.
|
||||
placement={position}
|
||||
// Disable events to improve performance when many tooltips
|
||||
// are shown (Quality Definitions for example).
|
||||
eventsEnabled={false}
|
||||
modifiers={{
|
||||
computeMaxHeight: {
|
||||
order: 851,
|
||||
enabled: true,
|
||||
fn: computeMaxSize,
|
||||
},
|
||||
preventOverflow: {
|
||||
// Fixes positioning for tooltips in the queue
|
||||
// and likely others.
|
||||
escapeWithReference: false,
|
||||
},
|
||||
flip: {
|
||||
enabled: canFlip,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
|
||||
updater.current = scheduleUpdate;
|
||||
|
||||
const popperPlacement = placement
|
||||
? placement.split('-')[0]
|
||||
: position;
|
||||
const vertical =
|
||||
popperPlacement === 'top' || popperPlacement === 'bottom';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
styles.tooltipContainer,
|
||||
vertical
|
||||
? styles.verticalContainer
|
||||
: styles.horizontalContainer
|
||||
)}
|
||||
style={style}
|
||||
onMouseEnter={handleMouseEnterTooltip}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div
|
||||
ref={arrowProps.ref}
|
||||
className={
|
||||
isOpen
|
||||
? classNames(
|
||||
styles.arrow,
|
||||
styles[kind],
|
||||
// @ts-expect-error - is a string that may not exist in styles
|
||||
styles[popperPlacement]
|
||||
)
|
||||
: styles.arrowDisabled
|
||||
}
|
||||
style={arrowProps.style}
|
||||
/>
|
||||
{isOpen ? (
|
||||
<div className={classNames(styles.tooltip, styles[kind])}>
|
||||
<div className={bodyClassName}>{tooltip}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Popper>
|
||||
</Portal>
|
||||
</Manager>
|
||||
<FloatingArrow ref={arrowRef} context={context} fill={arrowColor} />
|
||||
<div className={classNames(styles.tooltip, styles[kind])}>
|
||||
<div className={bodyClassName}>{tooltip}</div>
|
||||
</div>
|
||||
</div>
|
||||
</FloatingPortal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
27
frontend/src/Helpers/Hooks/useTheme.ts
Normal file
27
frontend/src/Helpers/Hooks/useTheme.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
|
||||
(theme) => {
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const useTheme = () => {
|
||||
return useSelector(createThemeSelector());
|
||||
};
|
||||
|
||||
export default useTheme;
|
||||
|
||||
export const useThemeColor = (color: string) => {
|
||||
const theme = useTheme();
|
||||
const themeVariables = themes[theme];
|
||||
|
||||
// @ts-expect-error - themeVariables is a string indexable type
|
||||
return themeVariables[color];
|
||||
};
|
|
@ -21,6 +21,7 @@
|
|||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.27.5",
|
||||
"@fortawesome/fontawesome-free": "6.7.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.1",
|
||||
|
|
41
yarn.lock
41
yarn.lock
|
@ -1033,6 +1033,42 @@
|
|||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
|
||||
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
|
||||
|
||||
"@floating-ui/core@^1.6.0":
|
||||
version "1.6.9"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.9.tgz#64d1da251433019dafa091de9b2886ff35ec14e6"
|
||||
integrity sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.9"
|
||||
|
||||
"@floating-ui/dom@^1.0.0":
|
||||
version "1.6.13"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34"
|
||||
integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.6.0"
|
||||
"@floating-ui/utils" "^0.2.9"
|
||||
|
||||
"@floating-ui/react-dom@^2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31"
|
||||
integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.0.0"
|
||||
|
||||
"@floating-ui/react@0.27.5":
|
||||
version "0.27.5"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.5.tgz#27a6e63a8ef35eb8712ef304a154ea706da26814"
|
||||
integrity sha512-BX3jKxo39Ba05pflcQmqPPwc0qdNsdNi/eweAFtoIdrJWNen2sVEWMEac3i6jU55Qfx+lOcdMNKYn2CtWmlnOQ==
|
||||
dependencies:
|
||||
"@floating-ui/react-dom" "^2.1.2"
|
||||
"@floating-ui/utils" "^0.2.9"
|
||||
tabbable "^6.0.0"
|
||||
|
||||
"@floating-ui/utils@^0.2.9":
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429"
|
||||
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
|
||||
|
||||
"@fortawesome/fontawesome-common-types@6.7.1":
|
||||
version "6.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz#6201640f39fdcf8e41cd9d1a92b2da3a96817fa4"
|
||||
|
@ -6514,6 +6550,11 @@ svg-tags@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
|
||||
integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
|
||||
|
||||
tabbable@^6.0.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
|
||||
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
|
||||
|
||||
table@^6.8.1:
|
||||
version "6.8.2"
|
||||
resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue