feat: 2.0 popover style updates (#3404)

This commit is contained in:
Jarrod Flesch
2023-09-29 09:05:02 -04:00
committed by GitHub
parent f723b0e0e9
commit 6fa5866f99
23 changed files with 400 additions and 460 deletions

View File

@@ -11,16 +11,6 @@
} }
} }
&.popup--active .array-actions__button {
background: var(--theme-elevation-0);
}
&__button,
&__action {
@extend %btn-reset;
cursor: pointer;
}
&__actions { &__actions {
list-style: none; list-style: none;
margin: 0; margin: 0;
@@ -28,9 +18,6 @@
} }
&__action { &__action {
@extend %btn-reset;
display: block;
svg { svg {
position: relative; position: relative;
top: -1px; top: -1px;
@@ -40,10 +27,6 @@
stroke-width: 1px; stroke-width: 1px;
} }
} }
&:hover {
opacity: 0.7;
}
} }
&__move-up { &__move-up {

View File

@@ -9,6 +9,7 @@ import More from '../../icons/More'
import Plus from '../../icons/Plus' import Plus from '../../icons/Plus'
import X from '../../icons/X' import X from '../../icons/X'
import Popup from '../Popup' import Popup from '../Popup'
import * as PopupList from '../Popup/PopupButtonList'
import './index.scss' import './index.scss'
const baseClass = 'array-actions' const baseClass = 'array-actions'
@@ -31,73 +32,69 @@ export const ArrayAction: React.FC<Props> = ({
horizontalAlign="center" horizontalAlign="center"
render={({ close }) => { render={({ close }) => {
return ( return (
<React.Fragment> <PopupList.ButtonGroup buttonSize="small">
{index !== 0 && ( {index !== 0 && (
<button <PopupList.Button
className={`${baseClass}__action ${baseClass}__move-up`} className={`${baseClass}__action ${baseClass}__move-up`}
onClick={() => { onClick={() => {
moveRow(index, index - 1) moveRow(index, index - 1)
close() close()
}} }}
type="button"
> >
<Chevron /> <Chevron />
{t('moveUp')} {t('moveUp')}
</button> </PopupList.Button>
)} )}
{index < rowCount - 1 && ( {index < rowCount - 1 && (
<button <PopupList.Button
className={`${baseClass}__action ${baseClass}__move-down`} className={`${baseClass}__action`}
onClick={() => { onClick={() => {
moveRow(index, index + 1) moveRow(index, index + 1)
close() close()
}} }}
type="button"
> >
<Chevron /> <Chevron />
{t('moveDown')} {t('moveDown')}
</button> </PopupList.Button>
)} )}
{!hasMaxRows && ( {!hasMaxRows && (
<React.Fragment> <React.Fragment>
<button <PopupList.Button
className={`${baseClass}__action ${baseClass}__add`} className={`${baseClass}__action ${baseClass}__add`}
onClick={() => { onClick={() => {
addRow(index + 1) addRow(index + 1)
close() close()
}} }}
type="button"
> >
<Plus /> <Plus />
{t('addBelow')} {t('addBelow')}
</button> </PopupList.Button>
<button <PopupList.Button
className={`${baseClass}__action ${baseClass}__duplicate`} className={`${baseClass}__action ${baseClass}__duplicate`}
onClick={() => { onClick={() => {
duplicateRow(index) duplicateRow(index)
close() close()
}} }}
type="button"
> >
<Copy /> <Copy />
{t('duplicate')} {t('duplicate')}
</button> </PopupList.Button>
</React.Fragment> </React.Fragment>
)} )}
<button <PopupList.Button
className={`${baseClass}__action ${baseClass}__remove`} className={`${baseClass}__action ${baseClass}__remove`}
onClick={() => { onClick={() => {
removeRow(index) removeRow(index)
close() close()
}} }}
type="button"
> >
<X /> <X />
{t('remove')} {t('remove')}
</button> </PopupList.Button>
</React.Fragment> </PopupList.ButtonGroup>
) )
}} }}
size="medium"
/> />
) )
} }

View File

@@ -73,13 +73,14 @@
} }
&__controls-wrapper { &__controls-wrapper {
--controls-gap: calc(var(--base) / 2);
--dot-button-width: calc(var(--base) * 2);
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0; margin: 0;
// move to the right to account for the padding on the dots gap: var(--controls-gap);
// this will make sure the alignment is correct padding-right: calc(var(--controls-gap) + var(--dot-button-width));
// while still keeping a large button hitbox position: relative;
transform: translate3d(var(--base), 0, 0);
} }
&__controls { &__controls {
@@ -94,27 +95,20 @@
} }
} }
&__popup {
.popup-button {
padding: var(--base);
background: transparent;
border: none;
cursor: pointer;
color: var(--theme-elevation-500);
&:hover {
color: var(--theme-text);
}
}
}
&__dots { &__dots {
display: flex; height: 100%;
gap: 2px; margin: 0;
background-color: transparent;
padding: 0;
> div { .btn__label {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 2px;
}
> span > span > div {
width: 3px; width: 3px;
height: 3px; height: 3px;
border-radius: 100%; border-radius: 100%;
@@ -122,49 +116,15 @@
} }
} }
&__popup-actions { &__popup {
list-style: none; position: absolute;
padding: 0; right: 0;
margin: 0; top: 0;
display: flex; bottom: 0;
flex-direction: column; width: var(--dot-button-width);
gap: calc(var(--base) / 4);
a { .popup__trigger-wrap {
text-decoration: none; height: 100%;
}
li {
position: relative;
&::before {
content: '';
display: block;
position: absolute;
height: 100%;
width: 100%;
border-radius: 1px;
background: var(--theme-elevation-100);
width: calc(100% + (var(--base) / 2));
left: calc(var(--base) / -4);
top: 0;
opacity: 0;
transition: opacity 50ms linear;
}
&:hover::before {
opacity: 1;
}
> * {
position: relative;
width: 100%;
height: 100%;
text-align: left;
font-size: inherit;
line-height: inherit;
font-family: inherit;
}
} }
} }

View File

@@ -1,6 +1,5 @@
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import type { CollectionPermission, GlobalPermission } from '../../../../auth' import type { CollectionPermission, GlobalPermission } from '../../../../auth'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../exports/types' import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../exports/types'
@@ -9,10 +8,12 @@ import { formatDate } from '../../../utilities/formatDate'
import { useConfig } from '../../utilities/Config' import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo' import { useDocumentInfo } from '../../utilities/DocumentInfo'
import Autosave from '../Autosave' import Autosave from '../Autosave'
import Button from '../Button'
import DeleteDocument from '../DeleteDocument' import DeleteDocument from '../DeleteDocument'
import DuplicateDocument from '../DuplicateDocument' import DuplicateDocument from '../DuplicateDocument'
import { Gutter } from '../Gutter' import { Gutter } from '../Gutter'
import Popup from '../Popup' import Popup from '../Popup'
import * as PopupList from '../Popup/PopupButtonList'
import PreviewButton from '../PreviewButton' import PreviewButton from '../PreviewButton'
import { Publish } from '../Publish' import { Publish } from '../Publish'
import { Save } from '../Save' import { Save } from '../Save'
@@ -30,9 +31,9 @@ export const DocumentControls: React.FC<{
global?: SanitizedGlobalConfig global?: SanitizedGlobalConfig
hasSavePermission?: boolean hasSavePermission?: boolean
id?: string id?: string
isAccountView?: boolean
isEditing?: boolean isEditing?: boolean
permissions?: CollectionPermission | GlobalPermission permissions?: CollectionPermission | GlobalPermission
isAccountView?: boolean
}> = (props) => { }> = (props) => {
const { const {
id, id,
@@ -41,9 +42,9 @@ export const DocumentControls: React.FC<{
disableActions, disableActions,
global, global,
hasSavePermission, hasSavePermission,
isAccountView,
isEditing, isEditing,
permissions, permissions,
isAccountView,
} = props } = props
const { publishedDoc } = useDocumentInfo() const { publishedDoc } = useDocumentInfo()
@@ -73,6 +74,8 @@ export const DocumentControls: React.FC<{
!global?.versions?.drafts?.autosave !global?.versions?.drafts?.autosave
} }
const showDotMenu = Boolean(collection && !disableActions)
return ( return (
<Gutter className={baseClass}> <Gutter className={baseClass}>
<div className={`${baseClass}__wrapper`}> <div className={`${baseClass}__wrapper`}>
@@ -187,49 +190,47 @@ export const DocumentControls: React.FC<{
</React.Fragment> </React.Fragment>
)} )}
</div> </div>
{Boolean(collection && !disableActions) && ( {showDotMenu && (
<Popup <Popup
button={ button={
<div className={`${baseClass}__dots`}> <Button buttonStyle="secondary" className={`${baseClass}__dots`} el="div">
<div /> <div />
<div /> <div />
<div /> <div />
</div> </Button>
} }
caret={false}
className={`${baseClass}__popup`} className={`${baseClass}__popup`}
horizontalAlign="center" horizontalAlign="right"
size="large" size="large"
verticalAlign="bottom" verticalAlign="bottom"
> >
<ul className={`${baseClass}__popup-actions`}> <PopupList.ButtonGroup>
{'create' in permissions && permissions?.create?.permission && ( {'create' in permissions && permissions?.create?.permission && (
<React.Fragment> <React.Fragment>
<li> <PopupList.Button
<Link id="action-create"
id="action-create" to={`${adminRoute}/collections/${collection?.slug}/create`}
to={`${adminRoute}/collections/${collection?.slug}/create`} >
> {t('createNew')}
{t('createNew')} </PopupList.Button>
</Link>
</li>
{!collection?.admin?.disableDuplicate && isEditing && ( {!collection?.admin?.disableDuplicate && isEditing && (
<li> <PopupList.Button>
<DuplicateDocument <DuplicateDocument
collection={collection} collection={collection}
id={id} id={id}
slug={collection?.slug} slug={collection?.slug}
/> />
</li> </PopupList.Button>
)} )}
</React.Fragment> </React.Fragment>
)} )}
{'delete' in permissions && permissions?.delete?.permission && id && ( {'delete' in permissions && permissions?.delete?.permission && id && (
<li> <PopupList.Button>
<DeleteDocument buttonId="action-delete" collection={collection} id={id} /> <DeleteDocument buttonId="action-delete" collection={collection} id={id} />
</li> </PopupList.Button>
)} )}
</ul> </PopupList.ButtonGroup>
</Popup> </Popup>
)} )}
</div> </div>

View File

@@ -18,7 +18,7 @@
button { button {
color: currentColor; color: currentColor;
padding: base(0.25) 0; padding: 0;
font-size: 1rem; font-size: 1rem;
line-height: base(1); line-height: base(1);
background: transparent; background: transparent;
@@ -39,33 +39,10 @@
&__button { &__button {
white-space: nowrap; white-space: nowrap;
display: flex;
} }
span { span {
color: var(--theme-elevation-400); color: var(--theme-elevation-400);
} }
ul {
list-style: none;
padding: 0;
max-height: base(8);
margin: 0;
li a {
all: unset;
cursor: pointer;
padding-right: 0;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
}
@include mid-break {
.popup__content {
width: calc(100vw - calc(var(--gutter-h) * 2));
}
}
} }

View File

@@ -1,13 +1,13 @@
import qs from 'qs' import qs from 'qs'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { Chevron } from '../..' import { Chevron } from '../..'
import { useConfig } from '../../utilities/Config' import { useConfig } from '../../utilities/Config'
import { useLocale } from '../../utilities/Locale' import { useLocale } from '../../utilities/Locale'
import { useSearchParams } from '../../utilities/SearchParams' import { useSearchParams } from '../../utilities/SearchParams'
import Popup from '../Popup' import Popup from '../Popup'
import * as PopupList from '../Popup/PopupButtonList'
import './index.scss' import './index.scss'
const baseClass = 'localizer' const baseClass = 'localizer'
@@ -37,20 +37,10 @@ const Localizer: React.FC<{
<Chevron className={`${baseClass}__chevron`} /> <Chevron className={`${baseClass}__chevron`} />
</div> </div>
} }
caret={false} horizontalAlign="right"
horizontalAlign="left"
render={({ close }) => ( render={({ close }) => (
<ul> <PopupList.ButtonGroup>
{locales.map((localeOption) => { {locales.map((localeOption) => {
const baseLocaleClass = `${baseClass}__locale`
const localeClasses = [
baseLocaleClass,
locale.code === localeOption.code && `${baseLocaleClass}--active`,
]
.filter(Boolean)
.join('')
const newParams = { const newParams = {
...searchParams, ...searchParams,
locale: localeOption.code, locale: localeOption.code,
@@ -60,20 +50,19 @@ const Localizer: React.FC<{
if (localeOption.code !== locale.code) { if (localeOption.code !== locale.code) {
return ( return (
<li className={localeClasses} key={localeOption.code}> <PopupList.Button key={localeOption.code} onClick={close} to={{ search }}>
<Link onClick={close} to={{ search }}> {localeOption.label}
{localeOption.label} {localeOption.label !== localeOption.code && ` (${localeOption.code})`}
{localeOption.label !== localeOption.code && ` (${localeOption.code})`} </PopupList.Button>
</Link>
</li>
) )
} }
return null return null
})} })}
</ul> </PopupList.ButtonGroup>
)} )}
showScrollbar showScrollbar
size="large"
/> />
</div> </div>
) )

View File

@@ -7,12 +7,6 @@
margin: 0; margin: 0;
} }
.popup-button--default {
@extend %btn-reset;
position: relative;
cursor: pointer;
}
&__button { &__button {
@extend %btn-reset; @extend %btn-reset;
cursor: pointer; cursor: pointer;

View File

@@ -7,6 +7,7 @@ import { defaults } from '../../../../collections/config/defaults'
import Chevron from '../../icons/Chevron' import Chevron from '../../icons/Chevron'
import { useSearchParams } from '../../utilities/SearchParams' import { useSearchParams } from '../../utilities/SearchParams'
import Popup from '../Popup' import Popup from '../Popup'
import * as PopupList from '../Popup/PopupButtonList'
import './index.scss' import './index.scss'
const baseClass = 'per-page' const baseClass = 'per-page'
@@ -43,43 +44,37 @@ const PerPage: React.FC<Props> = ({
} }
horizontalAlign="right" horizontalAlign="right"
render={({ close }) => ( render={({ close }) => (
<div> <PopupList.ButtonGroup>
<ul> {limits.map((limitNumber, i) => (
{limits.map((limitNumber, i) => ( <PopupList.Button
<li className={`${baseClass}-item`} key={i}> className={[limitNumber === Number(limit) && `${baseClass}__button-active`]
<button .filter(Boolean)
className={[ .join(' ')}
`${baseClass}__button`, key={i}
limitNumber === Number(limit) && `${baseClass}__button-active`, onClick={() => {
] close()
.filter(Boolean) if (handleChange) handleChange(limitNumber)
.join(' ')} if (modifySearchParams) {
onClick={() => { history.replace({
close() search: qs.stringify(
if (handleChange) handleChange(limitNumber) {
if (modifySearchParams) { ...params,
history.replace({ limit: limitNumber,
search: qs.stringify( page: resetPage ? 1 : params.page,
{ },
...params, { addQueryPrefix: true },
limit: limitNumber, ),
page: resetPage ? 1 : params.page, })
}, }
{ addQueryPrefix: true }, }}
), >
}) {limitNumber === Number(limit) && <Chevron />}
} {limitNumber}
}} </PopupList.Button>
type="button" ))}
> </PopupList.ButtonGroup>
{limitNumber === Number(limit) && <Chevron />}
{limitNumber}
</button>
</li>
))}
</ul>
</div>
)} )}
size="small"
/> />
</div> </div>
) )

View File

@@ -1,5 +0,0 @@
@import '../../../../scss/styles.scss';
.popup-button {
display: inline-flex;
}

View File

@@ -0,0 +1,47 @@
@import '../../../../scss/styles.scss';
.popup-button-list {
display: flex;
flex-direction: column;
text-align: left;
--list-button-padding: calc(var(--base) * 0.5);
&__text-align--left {
text-align: left;
}
&__text-align--center {
text-align: center;
}
&__text-align--right {
text-align: right;
}
&__button {
@extend %btn-reset;
padding-left: var(--list-button-padding);
padding-right: var(--list-button-padding);
padding-top: 2px;
padding-bottom: 2px;
cursor: pointer;
text-align: inherit;
line-height: var(--base);
text-decoration: none;
border-radius: 3px;
button {
@extend %btn-reset;
&:focus-visible {
outline: none;
}
}
&:hover,
&:focus-visible,
&:focus-within {
background-color: var(--popup-button-highlight);
}
}
}

View File

@@ -0,0 +1,75 @@
import type { LinkProps } from 'react-router-dom'
import * as React from 'react'
import { Link } from 'react-router-dom'
import './index.scss'
const baseClass = 'popup-button-list'
export const ButtonGroup: React.FC<{
buttonSize?: 'default' | 'small'
children: React.ReactNode
className?: string
textAlign?: 'center' | 'left' | 'right'
}> = ({ buttonSize = 'default', children, className, textAlign = 'left' }) => {
const classes = [
baseClass,
className,
`${baseClass}__text-align--${textAlign}`,
`${baseClass}__button-size--${buttonSize}`,
]
.filter(Boolean)
.join(' ')
return <div className={classes}>{children}</div>
}
type MenuButtonProps = {
children: React.ReactNode
className?: string
id?: string
onClick?: () => void
to?: LinkProps['to']
}
export const Button: React.FC<MenuButtonProps> = ({ id, children, className, onClick, to }) => {
const classes = [`${baseClass}__button`, className].filter(Boolean).join(' ')
if (to) {
return (
<Link
className={classes}
id={id}
onClick={() => {
if (onClick) {
onClick()
}
}}
to={to}
>
{children}
</Link>
)
}
if (onClick) {
return (
<button
className={classes}
id={id}
onClick={() => {
if (onClick) {
onClick()
}
}}
type="button"
>
{children}
</button>
)
}
return (
<div className={classes} id={id}>
{children}
</div>
)
}

View File

@@ -0,0 +1,14 @@
@import '../../../../scss/styles.scss';
.popup-button {
height: 100%;
color: currentColor;
padding: 0;
font-size: 1rem;
line-height: base(1);
background: transparent;
border: 0;
font-weight: 600;
cursor: pointer;
display: inline-flex;
}

View File

@@ -6,7 +6,7 @@ import './index.scss'
const baseClass = 'popup-button' const baseClass = 'popup-button'
const PopupButton: React.FC<Props> = (props) => { export const PopupTrigger: React.FC<Props> = (props) => {
const { active, button, buttonType, className, setActive } = props const { active, button, buttonType, className, setActive } = props
const classes = [baseClass, className, `${baseClass}--${buttonType}`].filter(Boolean).join(' ') const classes = [baseClass, className, `${baseClass}--${buttonType}`].filter(Boolean).join(' ')
@@ -41,5 +41,3 @@ const PopupButton: React.FC<Props> = (props) => {
</button> </button>
) )
} }
export default PopupButton

View File

@@ -1,43 +1,48 @@
@import '../../../scss/styles.scss'; @import '../../../scss/styles.scss';
.popup { .popup {
--popup-button-highlight: var(--theme-elevation-200);
--popup-bg: var(--theme-input-bg);
--popup-text: var(--theme-text);
--popup-caret-size: 10px;
--popup-x-padding: calc(var(--base) * 0.33);
--popup-padding: calc(var(--base) * 0.5);
position: relative; position: relative;
&__content { &__content {
position: absolute; position: absolute;
background: var(--theme-input-bg); background: var(--popup-bg);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
pointer-events: none; pointer-events: none;
z-index: var(--z-popup); z-index: var(--z-popup);
max-width: calc(100vw - #{$baseline}); max-width: calc(100vw - #{$baseline});
color: var(--popup-text);
&--caret { border-radius: 4px;
&:after { padding-left: var(--popup-padding);
content: ' '; padding-right: var(--popup-padding);
position: absolute; min-width: var(--popup-width, auto);
top: calc(100% - 1px);
border: 12px solid transparent;
border-top-color: var(--theme-input-bg);
}
}
} }
&__wrap { &__hide-scrollbar {
overflow: hidden; overflow: hidden;
} }
.popup__scroll { &__scroll-container {
padding: $baseline;
overflow-y: auto; overflow-y: auto;
white-space: nowrap; white-space: nowrap;
padding-right: calc(var(--scrollbar-width) + #{$baseline});
width: calc(100% + var(--scrollbar-width)); width: calc(100% + var(--scrollbar-width));
padding-top: var(--popup-padding);
padding-bottom: var(--popup-padding);
}
&__scroll-content {
width: calc(100% - var(--scrollbar-width));
} }
&--show-scrollbar { &--show-scrollbar {
.popup__scroll { .popup__scroll-container,
padding-right: 0; .popup__scroll-content {
width: 100%; width: 100%;
} }
} }
@@ -52,63 +57,24 @@
//////////////////////////////// ////////////////////////////////
&--size-small { &--size-small {
.popup__scroll { --popup-width: 100px;
[dir='ltr'] & {
padding: base(0.75) calc(var(--scrollbar-width) + #{base(0.75)}) base(0.75) base(0.75);
}
[dir='rtl'] & {
padding: base(0.75) base(0.75) base(0.75) calc(var(--scrollbar-width) + #{base(0.75)});
}
}
.popup__content { .popup__content {
@include shadow-m; @include shadow-m;
} }
}
&.popup--h-align-left { &--size-medium {
.popup__content { --popup-width: 150px;
left: - base(0.5); .popup__content {
@include shadow-lg;
&:after {
left: base(0.425);
}
}
} }
} }
&--size-large { &--size-large {
--popup-width: 200px;
.popup__content { .popup__content {
@include shadow-lg; @include shadow-lg;
} }
.popup__scroll {
padding: base(1) calc(var(--scrollbar-width) + #{base(1.5)}) base(1) base(1.5);
}
}
&--size-wide {
.popup__content {
@include shadow-m;
&:after {
border: 12px solid transparent;
border-top-color: var(--theme-input-bg);
}
}
.popup__scroll {
padding: base(0.25) base(0.5);
}
&.popup--align-left {
.popup__content {
left: - base(0.5);
&:after {
left: base(0.425);
}
}
}
} }
//////////////////////////////// ////////////////////////////////
@@ -116,12 +82,8 @@
//////////////////////////////// ////////////////////////////////
&--h-align-left { &--h-align-left {
.popup__content { .popup__caret {
left: - base(1.75); left: var(--popup-padding);
&:after {
left: base(1.75);
}
} }
} }
@@ -129,26 +91,21 @@
.popup__content { .popup__content {
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
}
&:after { .popup__caret {
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
}
} }
} }
&--h-align-right { &--h-align-right {
.popup__content { .popup__content {
right: - base(1.75); right: 0;
[dir='rtl'] & { }
right: - base(0.75);
} .popup__caret {
&:after { right: var(--popup-padding);
right: base(1.75);
[dir='rtl'] & {
right: base(0.75);
}
}
} }
} }
@@ -156,46 +113,32 @@
// VERTICAL ALIGNMENT // VERTICAL ALIGNMENT
//////////////////////////////// ////////////////////////////////
&__caret {
position: absolute;
border: var(--popup-caret-size) solid transparent;
}
&--v-align-top { &--v-align-top {
.popup__content { .popup__content {
bottom: calc(100% + #{$baseline}); @include shadow-lg;
bottom: calc(100% + var(--popup-caret-size));
}
.popup__caret {
top: calc(100% - 1px);
border-top-color: var(--popup-bg);
} }
} }
&--v-align-bottom { &--v-align-bottom {
.popup__content { .popup__content {
@include shadow-lg-top; @include shadow-lg-top;
top: calc(100% + #{base(0.5)}); top: calc(100% + var(--popup-caret-size));
&:after {
top: unset;
bottom: calc(100% - 1px);
border-top-color: transparent !important;
border-bottom-color: var(--theme-input-bg);
}
} }
&.popup--color-dark { .popup__caret {
.popup__content { bottom: calc(100% - 1px);
&:after { border-bottom-color: var(--popup-bg);
border-bottom-color: var(--theme-elevation-800);
}
}
}
}
////////////////////////////////
// COLOR
////////////////////////////////
&--color-dark {
.popup__content {
background: var(--theme-elevation-800);
color: var(--theme-input-bg);
&:after {
border-top-color: var(--theme-elevation-800);
}
} }
} }
@@ -212,69 +155,55 @@
} }
@include mid-break { @include mid-break {
&__scroll, --popup-padding: calc(var(--base) * 0.25);
&--size-large .popup__scroll {
padding: base(0.75);
padding-right: calc(var(--scrollbar-width) + #{base(0.75)});
}
&--h-align-left {
.popup__content {
left: - base(0.5);
&:after {
left: base(0.5);
}
}
}
&--h-align-center { &--h-align-center {
.popup__content { .popup__content {
left: 50%; left: 50%;
transform: translateX(-0%); transform: translateX(-0%);
}
&:after { .popup__caret {
left: 50%; left: 50%;
transform: translateX(-0%); transform: translateX(-0%);
}
} }
} }
&--h-align-right { &--h-align-right {
.popup__content { .popup__content {
right: - base(0.5); right: 0;
}
&:after { .popup__caret {
right: base(0.5); right: var(--popup-padding);
}
} }
} }
&--force-h-align-left { &--force-h-align-left {
.popup__content { .popup__content {
left: - base(0.5); left: 0;
right: unset; right: unset;
transform: unset; transform: unset;
}
&:after { .popup__caret {
left: base(0.5); left: var(--popup-padding);
right: unset; right: unset;
transform: unset; transform: unset;
}
} }
} }
&--force-h-align-right { &--force-h-align-right {
.popup__content { .popup__content {
right: - base(0.5); right: 0;
left: unset; left: unset;
transform: unset; transform: unset;
}
&:after { .popup__caret {
right: base(0.5); right: var(--popup-padding);
left: unset; left: unset;
transform: unset; transform: unset;
}
} }
} }
} }

View File

@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
import type { Props } from './types' import type { Props } from './types'
import useIntersect from '../../../hooks/useIntersect' import useIntersect from '../../../hooks/useIntersect'
import PopupButton from './PopupButton' import { PopupTrigger } from './PopupTrigger'
import './index.scss' import './index.scss'
const baseClass = 'popup' const baseClass = 'popup'
@@ -18,16 +18,14 @@ const Popup: React.FC<Props> = (props) => {
caret = true, caret = true,
children, children,
className, className,
color = 'light',
forceOpen, forceOpen,
horizontalAlign: horizontalAlignFromProps = 'left', horizontalAlign: horizontalAlignFromProps = 'left',
initActive = false, initActive = false,
onToggleOpen, onToggleOpen,
padding,
render, render,
showOnHover = false, showOnHover = false,
showScrollbar = false, showScrollbar = false,
size = 'small', size = 'medium',
verticalAlign: verticalAlignFromProps = 'top', verticalAlign: verticalAlignFromProps = 'top',
} = props } = props
@@ -38,7 +36,6 @@ const Popup: React.FC<Props> = (props) => {
threshold: 1, threshold: 1,
}) })
const buttonRef = useRef(null)
const contentRef = useRef(null) const contentRef = useRef(null)
const [active, setActive] = useState(initActive) const [active, setActive] = useState(initActive)
const [verticalAlign, setVerticalAlign] = useState(verticalAlignFromProps) const [verticalAlign, setVerticalAlign] = useState(verticalAlignFromProps)
@@ -57,8 +54,8 @@ const Popup: React.FC<Props> = (props) => {
} = bounds } = bounds
let boundingTopPos = 100 let boundingTopPos = 100
let boundingRightPos = window.innerWidth let boundingRightPos = document.documentElement.clientWidth
let boundingBottomPos = window.innerHeight let boundingBottomPos = document.documentElement.clientHeight
let boundingLeftPos = 0 let boundingLeftPos = 0
if (boundingRef?.current) { if (boundingRef?.current) {
@@ -131,7 +128,6 @@ const Popup: React.FC<Props> = (props) => {
baseClass, baseClass,
className, className,
`${baseClass}--size-${size}`, `${baseClass}--size-${size}`,
`${baseClass}--color-${color}`,
`${baseClass}--v-align-${verticalAlign}`, `${baseClass}--v-align-${verticalAlign}`,
`${baseClass}--h-align-${horizontalAlign}`, `${baseClass}--h-align-${horizontalAlign}`,
active && `${baseClass}--active`, active && `${baseClass}--active`,
@@ -142,39 +138,35 @@ const Popup: React.FC<Props> = (props) => {
return ( return (
<div className={classes}> <div className={classes}>
<div className={`${baseClass}__wrapper`} ref={buttonRef}> <div className={`${baseClass}__trigger-wrap`}>
{showOnHover ? ( {showOnHover ? (
<div <div
className={`${baseClass}__on-hover-watch`} className={`${baseClass}__on-hover-watch`}
onMouseEnter={() => setActive(true)} onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)} onMouseLeave={() => setActive(false)}
> >
<PopupButton <PopupTrigger
{...{ active, button, buttonType, className: buttonClassName, setActive }} {...{ active, button, buttonType, className: buttonClassName, setActive }}
/> />
</div> </div>
) : ( ) : (
<PopupButton {...{ active, button, buttonType, className: buttonClassName, setActive }} /> <PopupTrigger
{...{ active, button, buttonType, className: buttonClassName, setActive }}
/>
)} )}
</div> </div>
<div <div className={`${baseClass}__content`} ref={contentRef}>
className={[`${baseClass}__content`, caret && `${baseClass}__content--caret`] <div className={`${baseClass}__hide-scrollbar`} ref={intersectionRef}>
.filter(Boolean) <div className={`${baseClass}__scroll-container`}>
.join(' ')} <div className={`${baseClass}__scroll-content`}>
ref={contentRef} {render && render({ close: () => setActive(false) })}
> {children && children}
<div className={`${baseClass}__wrap`} ref={intersectionRef}> </div>
<div
className={`${baseClass}__scroll`}
style={{
padding,
}}
>
{render && render({ close: () => setActive(false) })}
{children && children}
</div> </div>
</div> </div>
{caret && <div className={`${baseClass}__caret`} />}
</div> </div>
</div> </div>
) )

View File

@@ -9,15 +9,13 @@ export type Props = {
caret?: boolean caret?: boolean
children?: React.ReactNode children?: React.ReactNode
className?: string className?: string
color?: 'dark' | 'light'
forceOpen?: boolean forceOpen?: boolean
horizontalAlign?: 'center' | 'left' | 'right' horizontalAlign?: 'center' | 'left' | 'right'
initActive?: boolean initActive?: boolean
onToggleOpen?: (active: boolean) => void onToggleOpen?: (active: boolean) => void
padding?: CSSProperties['padding']
render?: (any) => React.ReactNode render?: (any) => React.ReactNode
showOnHover?: boolean showOnHover?: boolean
showScrollbar?: boolean showScrollbar?: boolean
size?: 'large' | 'small' | 'wide' size?: 'fit-content' | 'large' | 'medium' | 'small'
verticalAlign?: 'bottom' | 'top' verticalAlign?: 'bottom' | 'top'
} }

View File

@@ -4,7 +4,7 @@
display: flex; display: flex;
align-items: stretch; align-items: stretch;
.popup__wrapper { .popup__trigger-wrap {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
height: 100%; height: 100%;
@@ -22,27 +22,4 @@
display: flex; display: flex;
cursor: pointer; cursor: pointer;
} }
&__relations {
list-style: none;
margin: 0;
padding: 0;
li:not(:last-child) {
margin-bottom: base(0.375);
}
}
&__relation-button {
@extend %btn-reset;
cursor: pointer;
display: block;
padding: base(0.125) 0;
text-align: center;
width: 100%;
&:hover {
opacity: 0.7;
}
}
} }

View File

@@ -2,6 +2,7 @@ import React, { Fragment, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { SanitizedCollectionConfig } from '../../../../../../collections/config/types' import type { SanitizedCollectionConfig } from '../../../../../../collections/config/types'
import type { EditViewProps } from '../../../../views/types'
import type { Value } from '../types' import type { Value } from '../types'
import type { Props } from './types' import type { Props } from './types'
@@ -9,13 +10,13 @@ import { getTranslation } from '../../../../../../utilities/getTranslation'
import Button from '../../../../elements/Button' import Button from '../../../../elements/Button'
import { useDocumentDrawer } from '../../../../elements/DocumentDrawer' import { useDocumentDrawer } from '../../../../elements/DocumentDrawer'
import Popup from '../../../../elements/Popup' import Popup from '../../../../elements/Popup'
import * as PopupList from '../../../../elements/Popup/PopupButtonList'
import Tooltip from '../../../../elements/Tooltip' import Tooltip from '../../../../elements/Tooltip'
import Plus from '../../../../icons/Plus' import Plus from '../../../../icons/Plus'
import { useAuth } from '../../../../utilities/Auth' import { useAuth } from '../../../../utilities/Auth'
import { useConfig } from '../../../../utilities/Config' import { useConfig } from '../../../../utilities/Config'
import './index.scss' import './index.scss'
import { useRelatedCollections } from './useRelatedCollections' import { useRelatedCollections } from './useRelatedCollections'
import { EditViewProps } from '../../../../views/types'
const baseClass = 'relationship-add-new' const baseClass = 'relationship-add-new'
@@ -85,7 +86,7 @@ export const AddNewRelation: React.FC<Props> = ({
[relationTo, collectionConfig, dispatchOptions, i18n, hasMany, setValue, value, config], [relationTo, collectionConfig, dispatchOptions, i18n, hasMany, setValue, value, config],
) )
const onPopopToggle = useCallback((state) => { const onPopupToggle = useCallback((state) => {
setPopupOpen(state) setPopupOpen(state)
}, []) }, [])
@@ -161,31 +162,30 @@ export const AddNewRelation: React.FC<Props> = ({
} }
buttonType="custom" buttonType="custom"
horizontalAlign="center" horizontalAlign="center"
onToggleOpen={onPopopToggle} onToggleOpen={onPopupToggle}
render={({ close: closePopup }) => ( render={({ close: closePopup }) => (
<ul className={`${baseClass}__relations`}> <PopupList.ButtonGroup>
{relatedCollections.map((relatedCollection) => { {relatedCollections.map((relatedCollection) => {
if (permissions.collections[relatedCollection.slug].create.permission) { if (permissions.collections[relatedCollection.slug].create.permission) {
return ( return (
<li key={relatedCollection.slug}> <PopupList.Button
<button className={`${baseClass}__relation-button--${relatedCollection.slug}`}
className={`${baseClass}__relation-button ${baseClass}__relation-button--${relatedCollection.slug}`} key={relatedCollection.slug}
onClick={() => { onClick={() => {
closePopup() closePopup()
setSelectedCollection(relatedCollection.slug) setSelectedCollection(relatedCollection.slug)
}} }}
type="button" >
> {getTranslation(relatedCollection.labels.singular, i18n)}
{getTranslation(relatedCollection.labels.singular, i18n)} </PopupList.Button>
</button>
</li>
) )
} }
return null return null
})} })}
</ul> </PopupList.ButtonGroup>
)} )}
size="medium"
/> />
{collectionConfig && {collectionConfig &&
permissions.collections[collectionConfig.slug].create.permission && ( permissions.collections[collectionConfig.slug].create.permission && (

View File

@@ -85,8 +85,8 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
@mixin shadow-lg-top { @mixin shadow-lg-top {
box-shadow: box-shadow:
0 -2px 20px 7px rgba(0, 2, 4, 0.1), 0 -20px 35px -10px rgba(0, 2, 4, 0.2),
0 6px 4px -4px rgba(0, 2, 4, 0.02); 0 -6px 4px -4px rgba(0, 2, 4, 0.02);
} }
@mixin shadow { @mixin shadow {

View File

@@ -11,14 +11,13 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
.popup__scroll, .popup__hide-scrollbar,
.popup__wrap { .popup__scroll-container {
overflow: visible; overflow: visible;
} }
.popup__scroll { .popup__scroll-content {
white-space: pre; white-space: pre;
padding-right: base(0.5);
} }
} }
@@ -35,7 +34,7 @@
@extend %btn-reset; @extend %btn-reset;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
margin: 0 0 0 base(0.25); margin: 0;
&:hover, &:hover,
&:focus-visible { &:focus-visible {
@@ -48,7 +47,29 @@
max-width: base(8); max-width: base(8);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-right: base(0.25); border-radius: 2px;
&:hover {
background-color: var(--popup-button-highlight);
}
}
}
.rich-text-link__popup {
display: flex;
gap: calc(var(--base) * 0.25);
button {
&:hover {
.btn__icon {
background-color: var(--popup-button-highlight);
.fill {
fill: var(--theme-text);
}
.stroke {
stroke: var(--theme-text);
}
}
}
} }
} }

View File

@@ -159,6 +159,7 @@ export const LinkElement: React.FC<{
href={`${config.routes.admin}/collections/${element.doc.relationTo}/${element.doc.value}`} href={`${config.routes.admin}/collections/${element.doc.relationTo}/${element.doc.value}`}
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
title={`${config.routes.admin}/collections/${element.doc.relationTo}/${element.doc.value}`}
> >
label label
</a> </a>
@@ -170,6 +171,7 @@ export const LinkElement: React.FC<{
href={element.url} href={element.url}
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
title={element.url}
> >
{element.url} {element.url}
</a> </a>
@@ -200,7 +202,7 @@ export const LinkElement: React.FC<{
/> />
</div> </div>
)} )}
size="small" size="fit-content"
verticalAlign="bottom" verticalAlign="bottom"
/> />
</span> </span>

View File

@@ -321,13 +321,13 @@ describe('fields', () => {
const firstBlockSelector = blocksDrawer const firstBlockSelector = blocksDrawer
.locator('.blocks-drawer__blocks .blocks-drawer__block') .locator('.blocks-drawer__blocks .blocks-drawer__block')
.first() .first()
await expect(firstBlockSelector).toContainText('Text') await expect(firstBlockSelector).toContainText('Content')
await firstBlockSelector.click() await firstBlockSelector.click()
// ensure the block was appended to the rows // ensure the block was appended to the rows
const addedRow = page.locator('#field-blocks .blocks-field__row').last() const addedRow = page.locator('#field-blocks .blocks-field__row').last()
await expect(addedRow).toBeVisible() await expect(addedRow).toBeVisible()
await expect(addedRow.locator('.blocks-field__block-pill-text')).toContainText('Text') await expect(addedRow.locator('.blocks-field__block-pill-content')).toContainText('Content')
}) })
test('should open blocks drawer from block row and add below', async () => { test('should open blocks drawer from block row and add below', async () => {
@@ -348,13 +348,13 @@ describe('fields', () => {
const firstBlockSelector = blocksDrawer const firstBlockSelector = blocksDrawer
.locator('.blocks-drawer__blocks .blocks-drawer__block') .locator('.blocks-drawer__blocks .blocks-drawer__block')
.first() .first()
await expect(firstBlockSelector).toContainText('Text') await expect(firstBlockSelector).toContainText('Content')
await firstBlockSelector.click() await firstBlockSelector.click()
// ensure the block was inserted beneath the first in the rows // ensure the block was inserted beneath the first in the rows
const addedRow = page.locator('#field-blocks #blocks-row-1') const addedRow = page.locator('#field-blocks #blocks-row-1')
await expect(addedRow).toBeVisible() await expect(addedRow).toBeVisible()
await expect(addedRow.locator('.blocks-field__block-pill-text')).toContainText('Text') // went from `Number` to `Text` await expect(addedRow.locator('.blocks-field__block-pill-content')).toContainText('Content') // went from `Number` to `Content`
}) })
test('should use i18n block labels', async () => { test('should use i18n block labels', async () => {
@@ -488,18 +488,16 @@ describe('fields', () => {
await page.locator('#potentiallyEmptyArray-row-1 .array-actions__button').click() await page.locator('#potentiallyEmptyArray-row-1 .array-actions__button').click()
await page await page
.locator('#potentiallyEmptyArray-row-1 .popup__scroll .array-actions__remove') .locator('#potentiallyEmptyArray-row-1 .popup__scroll-container .array-actions__remove')
.click() .click()
await page.locator('#potentiallyEmptyArray-row-0 .array-actions__button').click() await page.locator('#potentiallyEmptyArray-row-0 .array-actions__button').click()
await page await page
.locator('#potentiallyEmptyArray-row-0 .popup__scroll .array-actions__remove') .locator('#potentiallyEmptyArray-row-0 .popup__scroll-container .array-actions__remove')
.click() .click()
const rows = await page.locator( const rows = page.locator('#field-potentiallyEmptyArray > .array-field__draggable-rows')
'#field-potentiallyEmptyArray > .array-field__draggable-rows',
)
expect(rows).not.toBeVisible() await expect(rows).toBeHidden()
}) })
test('should remove existing row', async () => { test('should remove existing row', async () => {
@@ -513,15 +511,13 @@ describe('fields', () => {
await page.locator('#potentiallyEmptyArray-row-0 .array-actions__button').click() await page.locator('#potentiallyEmptyArray-row-0 .array-actions__button').click()
await page await page
.locator( .locator(
'#potentiallyEmptyArray-row-0 .popup__scroll .array-actions__action.array-actions__remove', '#potentiallyEmptyArray-row-0 .popup__scroll-container .array-actions__action.array-actions__remove',
) )
.click() .click()
const rows = await page.locator( const rows = page.locator('#field-potentiallyEmptyArray > .array-field__draggable-rows')
'#field-potentiallyEmptyArray > .array-field__draggable-rows',
)
expect(rows).not.toBeVisible() await expect(rows).toBeHidden()
}) })
test('should add row after removing existing row', async () => { test('should add row after removing existing row', async () => {
@@ -537,7 +533,7 @@ describe('fields', () => {
await page.locator('#potentiallyEmptyArray-row-1 .array-actions__button').click() await page.locator('#potentiallyEmptyArray-row-1 .array-actions__button').click()
await page await page
.locator( .locator(
'#potentiallyEmptyArray-row-1 .popup__scroll .array-actions__action.array-actions__remove', '#potentiallyEmptyArray-row-1 .popup__scroll-container .array-actions__action.array-actions__remove',
) )
.click() .click()
await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click() await page.locator('#field-potentiallyEmptyArray > .array-field__add-row').click()