Introduce focus traps for ModalHost, add button role to DropZone

This commit is contained in:
Scott Nonnenberg 2021-10-04 10:14:00 -07:00 committed by GitHub
parent adaeb81c32
commit 48229332ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 371 additions and 294 deletions

View file

@ -1003,6 +1003,30 @@ Signal Desktop makes use of the following open source projects.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
## focus-trap-react
The MIT License (MIT)
Copyright (c) 2015 David Clark
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## form-data ## form-data
Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors

View file

@ -96,6 +96,7 @@
"fast-glob": "3.2.1", "fast-glob": "3.2.1",
"filesize": "3.6.1", "filesize": "3.6.1",
"firstline": "1.2.1", "firstline": "1.2.1",
"focus-trap-react": "8.8.1",
"form-data": "3.0.0", "form-data": "3.0.0",
"fs-extra": "5.0.0", "fs-extra": "5.0.0",
"fuse.js": "3.4.4", "fuse.js": "3.4.4",

View file

@ -47,7 +47,10 @@ export const DropZone: React.ComponentType<Props> = props => {
}, [isDragActive, onDragActive]); }, [isDragActive, onDragActive]);
return ( return (
<div {...getRootProps({ className: getClassName(props, isDragActive) })}> <div
{...getRootProps({ className: getClassName(props, isDragActive) })}
role="button"
>
<input {...getInputProps()} /> <input {...getInputProps()} />
<svg viewBox="0 0 36 36" width="36px" height="36px"> <svg viewBox="0 0 36 36" width="36px" height="36px">
<path d="M32 17.25H18.75V4h-1.5v13.25H4v1.5h13.25V32h1.5V18.75H32v-1.5z" /> <path d="M32 17.25H18.75V4h-1.5v13.25H4v1.5h13.25V32h1.5V18.75H32v-1.5z" />

View file

@ -4,6 +4,8 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import FocusTrap from 'focus-trap-react';
import { Theme, themeClassName } from '../util/theme'; import { Theme, themeClassName } from '../util/theme';
import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useEscapeHandling } from '../hooks/useEscapeHandling';
@ -56,17 +58,19 @@ export const ModalHost = React.memo(
return root return root
? createPortal( ? createPortal(
<div <FocusTrap>
role="presentation" <div
className={classNames( role="presentation"
'module-modal-host__overlay', className={classNames(
theme ? themeClassName(theme) : undefined 'module-modal-host__overlay',
)} theme ? themeClassName(theme) : undefined
onMouseDown={noMouseClose ? undefined : handleMouseDown} )}
onMouseUp={noMouseClose ? undefined : handleMouseUp} onMouseDown={noMouseClose ? undefined : handleMouseDown}
> onMouseUp={noMouseClose ? undefined : handleMouseUp}
{children} >
</div>, {children}
</div>
</FocusTrap>,
root root
) )
: null; : null;

View file

@ -18,6 +18,8 @@ import {
last, last,
zipObject, zipObject,
} from 'lodash'; } from 'lodash';
import FocusTrap from 'focus-trap-react';
import { Emoji } from './Emoji'; import { Emoji } from './Emoji';
import { dataByCategory, search } from './lib'; import { dataByCategory, search } from './lib';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
@ -301,124 +303,126 @@ export const EmojiPicker = React.memo(
); );
return ( return (
<div className="module-emoji-picker" ref={ref} style={style}> <FocusTrap>
<header className="module-emoji-picker__header"> <div className="module-emoji-picker" ref={ref} style={style}>
<button <header className="module-emoji-picker__header">
type="button" <button
onClick={handleToggleSearch} type="button"
title={i18n('EmojiPicker--search-placeholder')} onClick={handleToggleSearch}
className={classNames( title={i18n('EmojiPicker--search-placeholder')}
'module-emoji-picker__button', className={classNames(
'module-emoji-picker__button--icon', 'module-emoji-picker__button',
searchMode 'module-emoji-picker__button--icon',
? 'module-emoji-picker__button--icon--close' searchMode
: 'module-emoji-picker__button--icon--search' ? 'module-emoji-picker__button--icon--close'
: 'module-emoji-picker__button--icon--search'
)}
aria-label={i18n('EmojiPicker--search-placeholder')}
/>
{searchMode ? (
<div className="module-emoji-picker__header__search-field">
<input
ref={focusOnRender}
className="module-emoji-picker__header__search-field__input"
placeholder={i18n('EmojiPicker--search-placeholder')}
onChange={handleSearchChange}
/>
</div>
) : (
categories.map(cat =>
cat === 'recents' && firstRecent.length === 0 ? null : (
<button
type="button"
key={cat}
data-category={cat}
title={cat}
onClick={handleSelectCategory}
className={classNames(
'module-emoji-picker__button',
'module-emoji-picker__button--icon',
`module-emoji-picker__button--icon--${cat}`,
selectedCategory === cat
? 'module-emoji-picker__button--selected'
: null
)}
aria-label={i18n(`EmojiPicker__button--${cat}`)}
/>
)
)
)} )}
aria-label={i18n('EmojiPicker--search-placeholder')} </header>
/> {emojiGrid.length > 0 ? (
{searchMode ? ( <div>
<div className="module-emoji-picker__header__search-field"> <AutoSizer>
<input {({ width, height }) => (
ref={focusOnRender} <Grid
className="module-emoji-picker__header__search-field__input" key={searchText}
placeholder={i18n('EmojiPicker--search-placeholder')} className="module-emoji-picker__body"
onChange={handleSearchChange} width={width}
/> height={height}
columnCount={COL_COUNT}
columnWidth={38}
rowHeight={getRowHeight}
rowCount={emojiGrid.length}
cellRenderer={cellRenderer}
scrollToRow={scrollToRow}
scrollToAlignment="start"
onSectionRendered={onSectionRendered}
/>
)}
</AutoSizer>
</div> </div>
) : ( ) : (
categories.map(cat => <div
cat === 'recents' && firstRecent.length === 0 ? null : ( className={classNames(
'module-emoji-picker__body',
'module-emoji-picker__body--empty'
)}
>
{i18n('EmojiPicker--empty')}
<Emoji
shortName="slightly_frowning_face"
size={16}
style={{ marginLeft: '4px' }}
/>
</div>
)}
<footer className="module-emoji-picker__footer">
{Boolean(onClickSettings) && (
<button
aria-label={i18n('CustomizingPreferredReactions__title')}
className="module-emoji-picker__button module-emoji-picker__button--footer module-emoji-picker__button--settings"
onClick={onClickSettings}
title={i18n('CustomizingPreferredReactions__title')}
type="button"
/>
)}
<div className="module-emoji-picker__footer__skin-tones">
{[0, 1, 2, 3, 4, 5].map(tone => (
<button <button
type="button" type="button"
key={cat} key={tone}
data-category={cat} data-tone={tone}
title={cat} onClick={handlePickTone}
onClick={handleSelectCategory} title={i18n('EmojiPicker--skin-tone', [`${tone}`])}
className={classNames( className={classNames(
'module-emoji-picker__button', 'module-emoji-picker__button',
'module-emoji-picker__button--icon', 'module-emoji-picker__button--footer',
`module-emoji-picker__button--icon--${cat}`, selectedTone === tone
selectedCategory === cat
? 'module-emoji-picker__button--selected' ? 'module-emoji-picker__button--selected'
: null : null
)} )}
aria-label={i18n(`EmojiPicker__button--${cat}`)} >
/> <Emoji shortName="hand" skinTone={tone} size={20} />
) </button>
) ))}
)} </div>
</header> {Boolean(onClickSettings) && (
{emojiGrid.length > 0 ? ( <div className="module-emoji-picker__footer__settings-spacer" />
<div>
<AutoSizer>
{({ width, height }) => (
<Grid
key={searchText}
className="module-emoji-picker__body"
width={width}
height={height}
columnCount={COL_COUNT}
columnWidth={38}
rowHeight={getRowHeight}
rowCount={emojiGrid.length}
cellRenderer={cellRenderer}
scrollToRow={scrollToRow}
scrollToAlignment="start"
onSectionRendered={onSectionRendered}
/>
)}
</AutoSizer>
</div>
) : (
<div
className={classNames(
'module-emoji-picker__body',
'module-emoji-picker__body--empty'
)} )}
> </footer>
{i18n('EmojiPicker--empty')} </div>
<Emoji </FocusTrap>
shortName="slightly_frowning_face"
size={16}
style={{ marginLeft: '4px' }}
/>
</div>
)}
<footer className="module-emoji-picker__footer">
{Boolean(onClickSettings) && (
<button
aria-label={i18n('CustomizingPreferredReactions__title')}
className="module-emoji-picker__button module-emoji-picker__button--footer module-emoji-picker__button--settings"
onClick={onClickSettings}
title={i18n('CustomizingPreferredReactions__title')}
type="button"
/>
)}
<div className="module-emoji-picker__footer__skin-tones">
{[0, 1, 2, 3, 4, 5].map(tone => (
<button
type="button"
key={tone}
data-tone={tone}
onClick={handlePickTone}
title={i18n('EmojiPicker--skin-tone', [`${tone}`])}
className={classNames(
'module-emoji-picker__button',
'module-emoji-picker__button--footer',
selectedTone === tone
? 'module-emoji-picker__button--selected'
: null
)}
>
<Emoji shortName="hand" skinTone={tone} size={20} />
</button>
))}
</div>
{Boolean(onClickSettings) && (
<div className="module-emoji-picker__footer__settings-spacer" />
)}
</footer>
</div>
); );
} }
) )

View file

@ -3,6 +3,8 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import { useRestoreFocus } from '../../hooks/useRestoreFocus'; import { useRestoreFocus } from '../../hooks/useRestoreFocus';
import { StickerPackType, StickerType } from '../../state/ducks/stickers'; import { StickerPackType, StickerType } from '../../state/ducks/stickers';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
@ -146,187 +148,193 @@ export const StickerPicker = React.memo(
const showLongText = showPickerHint; const showLongText = showPickerHint;
return ( return (
<div className="module-sticker-picker" ref={ref} style={style}> <FocusTrap>
<div className="module-sticker-picker__header"> <div className="module-sticker-picker" ref={ref} style={style}>
<div className="module-sticker-picker__header__packs"> <div className="module-sticker-picker__header">
<div <div className="module-sticker-picker__header__packs">
className="module-sticker-picker__header__packs__slider" <div
style={{ className="module-sticker-picker__header__packs__slider"
transform: `translateX(-${getPacksPageOffset( style={{
packsPage, transform: `translateX(-${getPacksPageOffset(
packs.length packsPage,
)}px)`, packs.length
}} )}px)`,
> }}
{hasPacks ? ( >
<button {hasPacks ? (
type="button"
onClick={recentsHandler}
className={classNames({
'module-sticker-picker__header__button': true,
'module-sticker-picker__header__button--recents': true,
'module-sticker-picker__header__button--selected':
currentTab === 'recents',
})}
aria-label={i18n('stickers--StickerPicker--Recents')}
/>
) : null}
{packs.map((pack, i) => (
<button
type="button"
key={pack.id}
onClick={packsHandlers[i]}
className={classNames(
'module-sticker-picker__header__button',
{
'module-sticker-picker__header__button--selected':
currentTab === pack.id,
'module-sticker-picker__header__button--error':
pack.status === 'error',
}
)}
>
{pack.cover ? (
<img
className="module-sticker-picker__header__button__image"
src={pack.cover.url}
alt={pack.title}
title={pack.title}
/>
) : (
<div className="module-sticker-picker__header__button__image-placeholder" />
)}
</button>
))}
</div>
{!isUsingKeyboard && packsPage > 0 ? (
<button
type="button"
className={classNames(
'module-sticker-picker__header__button',
'module-sticker-picker__header__button--prev-page'
)}
onClick={onClickPrevPackPage}
aria-label={i18n('stickers--StickerPicker--PrevPage')}
/>
) : null}
{!isUsingKeyboard && !isLastPacksPage(packsPage, packs.length) ? (
<button
type="button"
className={classNames(
'module-sticker-picker__header__button',
'module-sticker-picker__header__button--next-page'
)}
onClick={onClickNextPackPage}
aria-label={i18n('stickers--StickerPicker--NextPage')}
/>
) : null}
</div>
<button
type="button"
ref={addPackRef}
className={classNames(
'module-sticker-picker__header__button',
'module-sticker-picker__header__button--add-pack',
{
'module-sticker-picker__header__button--hint': showPickerHint,
}
)}
onClick={onClickAddPack}
aria-label={i18n('stickers--StickerPicker--AddPack')}
/>
</div>
<div
className={classNames('module-sticker-picker__body', {
'module-sticker-picker__body--empty': isEmpty,
})}
>
{showPickerHint ? (
<div
className={classNames(
'module-sticker-picker__body__text',
'module-sticker-picker__body__text--hint',
{
'module-sticker-picker__body__text--pin': showEmptyText,
}
)}
>
{i18n('stickers--StickerPicker--Hint')}
</div>
) : null}
{!hasPacks ? (
<div className="module-sticker-picker__body__text">
{i18n('stickers--StickerPicker--NoPacks')}
</div>
) : null}
{pendingCount > 0 ? (
<div className="module-sticker-picker__body__text">
{i18n('stickers--StickerPicker--DownloadPending')}
</div>
) : null}
{downloadError ? (
<div
className={classNames(
'module-sticker-picker__body__text',
'module-sticker-picker__body__text--error'
)}
>
{stickers.length > 0
? i18n('stickers--StickerPicker--DownloadError')
: i18n('stickers--StickerPicker--Empty')}
</div>
) : null}
{hasPacks && showEmptyText ? (
<div
className={classNames('module-sticker-picker__body__text', {
'module-sticker-picker__body__text--error': !isRecents,
})}
>
{isRecents
? i18n('stickers--StickerPicker--NoRecents')
: i18n('stickers--StickerPicker--Empty')}
</div>
) : null}
{!isEmpty ? (
<div
className={classNames('module-sticker-picker__body__content', {
'module-sticker-picker__body__content--under-text': showText,
'module-sticker-picker__body__content--under-long-text': showLongText,
})}
>
{stickers.map(({ packId, id, url }, index: number) => {
const maybeFocusRef = index === 0 ? focusRef : undefined;
return (
<button <button
type="button" type="button"
ref={maybeFocusRef} onClick={recentsHandler}
key={`${packId}-${id}`} className={classNames({
className="module-sticker-picker__body__cell" 'module-sticker-picker__header__button': true,
onClick={() => onPickSticker(packId, id)} 'module-sticker-picker__header__button--recents': true,
> 'module-sticker-picker__header__button--selected':
<img currentTab === 'recents',
className="module-sticker-picker__body__cell__image" })}
src={url} aria-label={i18n('stickers--StickerPicker--Recents')}
alt={packTitle}
/>
</button>
);
})}
{Array(pendingCount)
.fill(0)
.map((_, i) => (
<div
// eslint-disable-next-line react/no-array-index-key
key={i}
className="module-sticker-picker__body__cell__placeholder"
role="presentation"
/> />
) : null}
{packs.map((pack, i) => (
<button
type="button"
key={pack.id}
onClick={packsHandlers[i]}
className={classNames(
'module-sticker-picker__header__button',
{
'module-sticker-picker__header__button--selected':
currentTab === pack.id,
'module-sticker-picker__header__button--error':
pack.status === 'error',
}
)}
>
{pack.cover ? (
<img
className="module-sticker-picker__header__button__image"
src={pack.cover.url}
alt={pack.title}
title={pack.title}
/>
) : (
<div className="module-sticker-picker__header__button__image-placeholder" />
)}
</button>
))} ))}
</div>
{!isUsingKeyboard && packsPage > 0 ? (
<button
type="button"
className={classNames(
'module-sticker-picker__header__button',
'module-sticker-picker__header__button--prev-page'
)}
onClick={onClickPrevPackPage}
aria-label={i18n('stickers--StickerPicker--PrevPage')}
/>
) : null}
{!isUsingKeyboard &&
!isLastPacksPage(packsPage, packs.length) ? (
<button
type="button"
className={classNames(
'module-sticker-picker__header__button',
'module-sticker-picker__header__button--next-page'
)}
onClick={onClickNextPackPage}
aria-label={i18n('stickers--StickerPicker--NextPage')}
/>
) : null}
</div> </div>
) : null} <button
type="button"
ref={addPackRef}
className={classNames(
'module-sticker-picker__header__button',
'module-sticker-picker__header__button--add-pack',
{
'module-sticker-picker__header__button--hint': showPickerHint,
}
)}
onClick={onClickAddPack}
aria-label={i18n('stickers--StickerPicker--AddPack')}
/>
</div>
<div
className={classNames('module-sticker-picker__body', {
'module-sticker-picker__body--empty': isEmpty,
})}
>
{showPickerHint ? (
<div
className={classNames(
'module-sticker-picker__body__text',
'module-sticker-picker__body__text--hint',
{
'module-sticker-picker__body__text--pin': showEmptyText,
}
)}
>
{i18n('stickers--StickerPicker--Hint')}
</div>
) : null}
{!hasPacks ? (
<div className="module-sticker-picker__body__text">
{i18n('stickers--StickerPicker--NoPacks')}
</div>
) : null}
{pendingCount > 0 ? (
<div className="module-sticker-picker__body__text">
{i18n('stickers--StickerPicker--DownloadPending')}
</div>
) : null}
{downloadError ? (
<div
className={classNames(
'module-sticker-picker__body__text',
'module-sticker-picker__body__text--error'
)}
>
{stickers.length > 0
? i18n('stickers--StickerPicker--DownloadError')
: i18n('stickers--StickerPicker--Empty')}
</div>
) : null}
{hasPacks && showEmptyText ? (
<div
className={classNames('module-sticker-picker__body__text', {
'module-sticker-picker__body__text--error': !isRecents,
})}
>
{isRecents
? i18n('stickers--StickerPicker--NoRecents')
: i18n('stickers--StickerPicker--Empty')}
</div>
) : null}
{!isEmpty ? (
<div
className={classNames(
'module-sticker-picker__body__content',
{
'module-sticker-picker__body__content--under-text': showText,
'module-sticker-picker__body__content--under-long-text': showLongText,
}
)}
>
{stickers.map(({ packId, id, url }, index: number) => {
const maybeFocusRef = index === 0 ? focusRef : undefined;
return (
<button
type="button"
ref={maybeFocusRef}
key={`${packId}-${id}`}
className="module-sticker-picker__body__cell"
onClick={() => onPickSticker(packId, id)}
>
<img
className="module-sticker-picker__body__cell__image"
src={url}
alt={packTitle}
/>
</button>
);
})}
{Array(pendingCount)
.fill(0)
.map((_, i) => (
<div
// eslint-disable-next-line react/no-array-index-key
key={i}
className="module-sticker-picker__body__cell__placeholder"
role="presentation"
/>
))}
</div>
) : null}
</div>
</div> </div>
</div> </FocusTrap>
); );
} }
) )

View file

@ -1688,6 +1688,20 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-09-15T00:38:04.183Z" "updated": "2018-09-15T00:38:04.183Z"
}, },
{
"rule": "React-findDOMNode",
"path": "node_modules/focus-trap-react/dist/focus-trap-react.js",
"line": " var focusTrapElementDOMNodes = this.focusTrapElements.map( // NOTE: `findDOMNode()` does not support CSS selectors; it'll just return",
"reasonCategory": "exampleCode",
"updated": "2021-10-01T23:53:26.107Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/focus-trap-react/src/focus-trap-react.js",
"line": " // NOTE: `findDOMNode()` does not support CSS selectors; it'll just return",
"reasonCategory": "exampleCode",
"updated": "2021-10-01T23:53:26.107Z"
},
{ {
"rule": "jQuery-load(", "rule": "jQuery-load(",
"path": "node_modules/foreground-child/node_modules/signal-exit/index.js", "path": "node_modules/foreground-child/node_modules/signal-exit/index.js",

View file

@ -8677,6 +8677,20 @@ focus-lock@^0.6.3:
resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.6.5.tgz#f6eb37832a9b1b205406175f5277396a28c0fce1" resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.6.5.tgz#f6eb37832a9b1b205406175f5277396a28c0fce1"
integrity sha512-i/mVBOoa9o+tl+u9owOJUF8k8L85odZNIsctB+JAK2HFT8jckiBwmk+3uydlm6FN8czgnkIwQtBv6yyAbrzXjw== integrity sha512-i/mVBOoa9o+tl+u9owOJUF8k8L85odZNIsctB+JAK2HFT8jckiBwmk+3uydlm6FN8czgnkIwQtBv6yyAbrzXjw==
focus-trap-react@8.8.1:
version "8.8.1"
resolved "https://registry.yarnpkg.com/focus-trap-react/-/focus-trap-react-8.8.1.tgz#2c7043e748460a191a1c7e2552e7c964f3f9fd46"
integrity sha512-Uy7U/l3fozlwLYBSQHP91QjuRUUcAQ9FJ3glAGmwF/fXSiPa/4negTy02zWElLZdZDPIfebC8V14aI1Gzc5V3w==
dependencies:
focus-trap "^6.7.1"
focus-trap@^6.7.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-6.7.1.tgz#d474f86dbaf3c7fbf0d53cf0b12295f4f4068d10"
integrity sha512-a6czHbT9twVpy2RpkWQA9vIgwQgB9Nx1PIxNNUxQT4nugG/3QibwxO+tWTh9i+zSY2SFiX4pnYhTaFaQF/6ZAg==
dependencies:
tabbable "^5.2.1"
follow-redirects@^1.0.0: follow-redirects@^1.0.0:
version "1.13.0" version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
@ -17445,6 +17459,11 @@ symbol.prototype.description@^1.0.0:
dependencies: dependencies:
has-symbols "^1.0.0" has-symbols "^1.0.0"
tabbable@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.2.1.tgz#e3fda7367ddbb172dcda9f871c0fdb36d1c4cd9c"
integrity sha512-40pEZ2mhjaZzK0BnI+QGNjJO8UYx9pP5v7BGe17SORTO0OEuuaAwQTkAp8whcZvqon44wKFOikD+Al11K3JICQ==
table@^5.2.3: table@^5.2.3:
version "5.4.6" version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"