Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Modernize styling of content picker #359

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
49 changes: 49 additions & 0 deletions components/content-picker/DraggableChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Flex, FlexItem } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import styled from '@emotion/styled';
import { DragHandle } from '../drag-handle';

const ChipWrapper = styled.div`
pointer-events: none;
`;

const Chip = styled.div`
background: #1e1e1e;
opacity: 0.9;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
color: #fff;
display: inline-flex;
margin: 0;
padding: 8px;
font-size: 13px;
line-height: 1.4;
white-space: nowrap;

svg {
fill: currentColor;
}
`;

interface DraggableChipProps {
title: string;
}

export const DraggableChip = (props: DraggableChipProps) => {
let { title = __('Moving 1 item', '10up-block-components') } = props;

if (!title) {
title = __('Moving 1 item', '10up-block-components');
}

return (
<ChipWrapper>
<Chip data-testid="draggable-chip">
<Flex justify="center" align="center" gap={4}>
<FlexItem>{title}</FlexItem>
<DragHandle />
</Flex>
</Chip>
</ChipWrapper>
);
};
275 changes: 125 additions & 150 deletions components/content-picker/PickedItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,80 +4,115 @@ import { CSS } from '@dnd-kit/utilities';
import { safeDecodeURI, filterURLForDisplay } from '@wordpress/url';
import { decodeEntities } from '@wordpress/html-entities';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { Post, User, store as coreStore } from '@wordpress/core-data';
import { close } from '@wordpress/icons';
import { Button, __experimentalTreeGridRow as TreeGridRow } from '@wordpress/components';
import { DragHandle } from '../drag-handle';
import { ContentSearchMode } from '../content-search/types';

type Term = {
count: number;
description: string;
export type PickedItemType = {
id: number;
link: string;
meta: { [key: string]: unknown };
name: string;
parent: number;
slug: string;
taxonomy: string;
type: string;
uuid: string;
title: string;
url: string;
};

const StyledCloseButton = styled('button')`
display: block;
padding: 2px 8px 6px 8px;
margin-left: auto;
font-size: 2em;
cursor: pointer;
border: none;
background-color: transparent;
const PickedItemContainer = styled.div<{ isDragging?: boolean; isOrderable?: boolean }>`
position: relative;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
min-height: 36px;
color: #1e1e1e;
opacity: ${({ isDragging }) => (isDragging ? 0.5 : 1)};
background: ${({ isDragging }) => (isDragging ? '#f0f0f0' : 'transparent')};
border-radius: 2px;
transition: background-color 0.1s linear;
cursor: ${({ isDragging, isOrderable }) => {
if (!isOrderable) return 'default';
return isDragging ? 'grabbing' : 'grab';
}};
touch-action: none;

&:hover {
background-color: #ccc;
background: #f0f0f0;
}

.components-button.has-icon {
min-width: 24px;
padding: 0;
height: 24px;
}

&:not(:hover) .remove-button {
opacity: 0;
pointer-events: none;
}
`;

function getEntityKind(mode: ContentSearchMode) {
let type;
switch (mode) {
case 'post':
type = 'postType' as const;
break;
case 'user':
type = 'root' as const;
break;
default:
type = 'taxonomy' as const;
break;
const DragHandleWrapper = styled.div<{ isDragging: boolean }>`
display: ${({ isDragging }) => (isDragging ? 'flex' : 'none')};
align-items: center;
justify-content: center;
opacity: ${({ isDragging }) => (isDragging ? 1 : 0)};
pointer-events: ${({ isDragging }) => (isDragging ? 'auto' : 'none')};
transition: opacity 0.1s linear;
position: absolute;
left: 8px;
`;

const RemoveButton = styled(Button)<{ isDragging?: boolean }>`
opacity: ${({ isDragging }) => (isDragging ? 0 : 1)};
pointer-events: ${({ isDragging }) => (isDragging ? 'none' : 'auto')};
transition: opacity 0.1s linear;

&:focus {
opacity: 1;
pointer-events: auto;
}
`;

return type;
}
const ItemContent = styled.div`
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
padding-left: ${({ isDragging }: { isDragging?: boolean }) => (isDragging ? '24px' : '0')};
transition: padding-left 0.1s linear;
`;

export type PickedItemType = {
id: number;
type: string;
uuid: string;
title: string;
url: string;
};
const ItemTitle = styled.span`
font-size: 13px;
line-height: 1.4;
font-weight: 500;
color: #1e1e1e;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;

const ItemURL = styled.span`
font-size: 12px;
line-height: 1.4;
color: #757575;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;

interface PickedItemProps {
item: PickedItemType;
isOrderable?: boolean;
handleItemDelete: (deletedItem: PickedItemType) => void;
mode: ContentSearchMode;
id: number | string;
isDragging?: boolean;
positionInSet?: number;
setSize?: number;
}

const PickedItemContainer = styled.span`
&&& {
align-items: flex-start;
display: flex;
flex-direction: column;
justify-content: space-between;
}
`;

/**
* PickedItem
*
Expand All @@ -88,117 +123,57 @@ const PickedItem: React.FC<PickedItemProps> = ({
item,
isOrderable = false,
handleItemDelete,
mode,
id,
isDragging = false,
positionInSet = 1,
setSize = 1,
}) => {
const entityKind = getEntityKind(mode);

const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id,
});

// This will return undefined while the item data is being fetched. If the item comes back
// empty, it will return null, which is handled in the effect below.
const preparedItem = useSelect(
(select) => {
// @ts-ignore-next-line - The WordPress types are missing the hasFinishedResolution method.
const { getEntityRecord, hasFinishedResolution } = select(coreStore);

const getEntityRecordParameters = [entityKind, item.type, item.id] as const;
const result = getEntityRecord<Post | Term | User>(...getEntityRecordParameters);

if (result) {
let newItem: Partial<PickedItemType>;

if (mode === 'post') {
const post = result as Post;
newItem = {
title: post.title.rendered,
url: post.link,
id: post.id,
type: post.type,
};
} else if (mode === 'user') {
const user = result as User;
newItem = {
title: user.name,
url: user.link,
id: user.id,
type: 'user',
};
} else {
const taxonomy = result as Term;
newItem = {
title: taxonomy.name,
url: taxonomy.link,
id: taxonomy.id,
type: taxonomy.taxonomy,
};
}

if (item.uuid) {
newItem.uuid = item.uuid;
}

return newItem as PickedItemType;
}

if (hasFinishedResolution('getEntityRecord', getEntityRecordParameters)) {
return null;
}

return undefined;
},
[item.id, entityKind],
);

// If `getEntityRecord` did not return an item, pass it to the delete callback.
useEffect(() => {
if (preparedItem === null) {
handleItemDelete(item);
}
}, [item, handleItemDelete, preparedItem]);

const style = {
transform: CSS.Transform.toString(transform),
transition,
border: isDragging ? '2px dashed #ddd' : '2px dashed transparent',
paddingTop: '10px',
paddingBottom: '10px',
display: 'flex',
alignItems: 'center',
paddingLeft: isOrderable ? '3px' : '8px',
};

const normalizedItemType = item?.type ? item.type : 'post';
const className = `block-editor-link-control__search-item is-entity is-type-${normalizedItemType}`;

if (!preparedItem) {
return null;
}

return (
<li className={className} ref={setNodeRef} style={style}>
{isOrderable ? <DragHandle {...attributes} {...listeners} /> : ''}
<PickedItemContainer className="block-editor-link-control__search-item-header">
<span className="block-editor-link-control__search-item-title">
{decodeEntities(preparedItem.title)}
</span>
<span aria-hidden className="block-editor-link-control__search-item-info">
{filterURLForDisplay(safeDecodeURI(preparedItem.url)) || ''}
</span>
</PickedItemContainer>

<StyledCloseButton
type="button"
onClick={() => {
handleItemDelete(preparedItem);
}}
aria-label={__('Delete item', '10up-block-components')}
<TreeGridRow level={1} positionInSet={positionInSet} setSize={setSize}>
<PickedItemContainer
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
isDragging={isDragging}
isOrderable={isOrderable}
>
&times;
</StyledCloseButton>
</li>
{isOrderable && (
<DragHandleWrapper isDragging={isDragging}>
<DragHandle />
</DragHandleWrapper>
)}
<ItemContent isDragging={isDragging}>
<ItemTitle>{decodeEntities(item.title)}</ItemTitle>
{item.url && (
<ItemURL>{filterURLForDisplay(safeDecodeURI(item.url)) || ''}</ItemURL>
)}
</ItemContent>
{!isDragging && (
<RemoveButton
className="remove-button"
icon={close}
size="small"
variant="tertiary"
isDestructive
label={__('Remove item', '10up-block-components')}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
handleItemDelete(item);
}}
/>
)}
</PickedItemContainer>
</TreeGridRow>
);
};

Expand Down
Loading
Loading