/**
* External dependencies
*/
import clsx from 'clsx';
/**
* WordPress dependencies
*/
import {
__experimentalHStack as HStack,
__experimentalVStack as VStack,
__experimentalSpacer as Spacer,
__experimentalItemGroup as ItemGroup,
__experimentalInputControl as InputControl,
__experimentalUnitControl as UnitControl,
__experimentalGrid as Grid,
__experimentalDropdownContentWrapper as DropdownContentWrapper,
useNavigator,
__experimentalToggleGroupControl as ToggleGroupControl,
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
__experimentalConfirmDialog as ConfirmDialog,
Dropdown,
Button,
Flex,
FlexItem,
ColorPalette,
Modal,
privateApis as componentsPrivateApis,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import {
plus,
shadow as shadowIcon,
reset,
moreVertical,
} from '@wordpress/icons';
import { useState, useMemo, useEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import { Subtitle } from './subtitle';
import { ScreenHeader } from './screen-header';
import { ScreenBody } from './screen-body';
import { defaultShadow } from './shadows-panel';
import {
getShadowParts,
shadowStringToObject,
shadowObjectToString,
} from './shadow-utils';
import { useSetting } from './hooks';
import { unlock } from './lock-unlock';
const { Menu } = unlock( componentsPrivateApis );
const customShadowMenuItems = [
{
label: __( 'Rename' ),
action: 'rename',
},
{
label: __( 'Delete' ),
action: 'delete',
},
];
const presetShadowMenuItems = [
{
label: __( 'Reset' ),
action: 'reset',
},
];
export default function ShadowsEditPanel() {
const { goBack, params } = useNavigator();
const { category, slug } = params;
const [ shadows, setShadows ] = useSetting(
`shadow.presets.${ category }`
);
useEffect( () => {
const hasCurrentShadow = shadows?.some(
( shadow: any ) => shadow.slug === slug
);
// If the shadow being edited doesn't exist anymore in the global styles setting, navigate back
// to prevent the user from editing a non-existent shadow entry.
// This can happen, for example:
// - when the user deletes the shadow
// - when the user resets the styles while editing a custom shadow
//
// The check on the slug is necessary to prevent a double back navigation when the user triggers
// a backward navigation by interacting with the screen's UI.
if ( !! slug && ! hasCurrentShadow ) {
goBack();
}
}, [ shadows, slug, goBack ] );
const [ baseShadows ] = useSetting(
`shadow.presets.${ category }`,
undefined,
'base'
);
const [ selectedShadow, setSelectedShadow ] = useState( () =>
( shadows || [] ).find( ( shadow: any ) => shadow.slug === slug )
);
const baseSelectedShadow = useMemo(
() => ( baseShadows || [] ).find( ( b: any ) => b.slug === slug ),
[ baseShadows, slug ]
);
const [ isConfirmDialogVisible, setIsConfirmDialogVisible ] =
useState( false );
const [ isRenameModalVisible, setIsRenameModalVisible ] = useState( false );
const [ shadowName, setShadowName ] = useState< string | undefined >(
selectedShadow?.name
);
if ( ! category || ! slug ) {
return null;
}
const onShadowChange = ( shadow: string ) => {
setSelectedShadow( { ...selectedShadow, shadow } );
const updatedShadows = shadows.map( ( s: any ) =>
s.slug === slug ? { ...selectedShadow, shadow } : s
);
setShadows( updatedShadows );
};
const onMenuClick = ( action: string ) => {
if ( action === 'reset' ) {
const updatedShadows = shadows.map( ( s: any ) =>
s.slug === slug ? baseSelectedShadow : s
);
setSelectedShadow( baseSelectedShadow );
setShadows( updatedShadows );
} else if ( action === 'delete' ) {
setIsConfirmDialogVisible( true );
} else if ( action === 'rename' ) {
setIsRenameModalVisible( true );
}
};
const handleShadowDelete = () => {
setShadows( shadows.filter( ( s: any ) => s.slug !== slug ) );
};
const handleShadowRename = ( newName: string | undefined ) => {
if ( ! newName ) {
return;
}
const updatedShadows = shadows.map( ( s: any ) =>
s.slug === slug ? { ...selectedShadow, name: newName } : s
);
setSelectedShadow( { ...selectedShadow, name: newName } );
setShadows( updatedShadows );
};
return ! selectedShadow ? (
) : (
<>
{ isConfirmDialogVisible && (
{
handleShadowDelete();
setIsConfirmDialogVisible( false );
} }
onCancel={ () => {
setIsConfirmDialogVisible( false );
} }
confirmButtonText={ __( 'Delete' ) }
size="medium"
>
{ sprintf(
/* translators: %s: Name of the shadow preset. */
__(
'Are you sure you want to delete "%s" shadow preset?'
),
selectedShadow.name
) }
) }
{ isRenameModalVisible && (
setIsRenameModalVisible( false ) }
size="small"
>
) }
>
);
}
interface ShadowsPreviewProps {
shadow: string;
}
function ShadowsPreview( { shadow }: ShadowsPreviewProps ) {
const shadowStyle = {
boxShadow: shadow,
};
return (
);
}
interface ShadowEditorProps {
shadow: string;
onChange: ( shadow: string ) => void;
}
function ShadowEditor( { shadow, onChange }: ShadowEditorProps ) {
const addShadowButtonRef = useRef< HTMLButtonElement >( null );
const shadowParts = useMemo( () => getShadowParts( shadow ), [ shadow ] );
const onChangeShadowPart = ( index: number, part: string ) => {
const newShadowParts = [ ...shadowParts ];
newShadowParts[ index ] = part;
onChange( newShadowParts.join( ', ' ) );
};
const onAddShadowPart = () => {
onChange( [ ...shadowParts, defaultShadow ].join( ', ' ) );
};
const onRemoveShadowPart = ( index: number ) => {
onChange( shadowParts.filter( ( p, i ) => i !== index ).join( ', ' ) );
addShadowButtonRef.current?.focus();
};
return (
<>
{ __( 'Shadows' ) }
{ shadowParts.map( ( part, index ) => (
onChangeShadowPart( index, value )
}
canRemove={ shadowParts.length > 1 }
onRemove={ () => onRemoveShadowPart( index ) }
/>
) ) }
>
);
}
interface ShadowItemProps {
shadow: string;
onChange: ( shadow: string ) => void;
canRemove: boolean;
onRemove: () => void;
}
function ShadowItem( {
shadow,
onChange,
canRemove,
onRemove,
}: ShadowItemProps ) {
const popoverProps = {
placement: 'left-start' as const,
offset: 36,
shift: true,
};
const shadowObj = useMemo(
() => shadowStringToObject( shadow ),
[ shadow ]
);
const onShadowChange = ( newShadow: any ) => {
onChange( shadowObjectToString( newShadow ) );
};
return (
{
const toggleProps = {
onClick: onToggle,
className: clsx(
'global-styles-ui__shadow-editor__dropdown-toggle',
{ 'is-open': isOpen }
),
'aria-expanded': isOpen,
};
const removeButtonProps = {
onClick: () => {
if ( isOpen ) {
onToggle();
}
onRemove();
},
className: clsx(
'global-styles-ui__shadow-editor__remove-button',
{ 'is-open': isOpen }
),
label: __( 'Remove shadow' ),
};
return (
<>
{ canRemove && (
) }
>
);
} }
renderContent={ () => (
) }
/>
);
}
interface ShadowPopoverProps {
shadowObj: any;
onChange: ( shadow: any ) => void;
}
function ShadowPopover( { shadowObj, onChange }: ShadowPopoverProps ) {
const __experimentalIsRenderedInSidebar = true;
const enableAlpha = true;
const onShadowChange = ( key: string, value: any ) => {
const newShadow = {
...shadowObj,
[ key ]: value,
};
onChange( newShadow );
};
return (
onShadowChange( 'color', value ) }
/>
onShadowChange( 'inset', value === 'inset' )
}
hideLabelFromVision
__next40pxDefaultSize
>
onShadowChange( 'x', value ) }
/>
onShadowChange( 'y', value ) }
/>
onShadowChange( 'blur', value ) }
/>
onShadowChange( 'spread', value ) }
/>
);
}
interface ShadowInputControlProps {
label: string;
value: string;
onChange: ( value: string ) => void;
}
function ShadowInputControl( {
label,
value,
onChange,
}: ShadowInputControlProps ) {
const onValueChange = ( next: string | undefined ) => {
const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) );
const nextValue = isNumeric ? next : '0px';
onChange( nextValue );
};
return (
);
}