Skip to content

Commit

Permalink
Merge pull request #5 from infinum/release/1.5.0
Browse files Browse the repository at this point in the history
1.5.0
  • Loading branch information
goranalkovic-infinum authored Sep 11, 2024
2 parents 10feeda + aa520bc commit d72420a
Show file tree
Hide file tree
Showing 16 changed files with 2,543 additions and 2,093 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file.

This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a CHANGELOG](https://keepachangelog.com/).

## [1.5.0] - 2024-09-11
- Updated dependencies.
- (**breaking-ish**) Tweaked CSS reset to ignore whole WP admin by default. You'll need to add `es-uic-has-css-reset` to enable it where needed.
- Reworked `DraggableList`, now using a new animation library for a more fluid experience.
- There's now also a `DraggableListItemHandle` that can be placed anywhere within `DraggableListItem` to mark the drag area.
- `DraggableListItem` will now hide the label properly if `title`, `icon` and `subtitle` are not sent
- `DraggableList` now supports a `labelAsHandle` prop to constrain dragging just to the label, instead of the whole item (not compatible with a custom handle!)
- Made `Switch` transforms harder to override by accident from an external source.
- Slightly tweaked `Repeater` styling.
- `LinkInput` should work properly now if `fetchSuggestions` is not passed.
- `LinkInput` will not show suggestions when the field is empty. You can opt out into that with `showSuggestionsWhenEmpty` (could be useful if you have a default list of suggestions).
- Added `Draggable` component for more random layouts.
- Added `ItemCollection` component to get rid of those pesky `.map`s in the editor.

## [1.4.7] - 2024-08-16
- Disabled focus handling in `Expandable` due to Gutenberg conflicts.
- Fixed `LinkInput` external value not previewing.
Expand Down Expand Up @@ -152,6 +166,7 @@ This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a

[Unreleased]: https://github.com/infinum/eightshift-ui-components/compare/master...HEAD

[1.5.0]: https://github.com/infinum/eightshift-ui-components/compare/1.4.7...1.5.0
[1.4.7]: https://github.com/infinum/eightshift-ui-components/compare/1.4.6...1.4.7
[1.4.6]: https://github.com/infinum/eightshift-ui-components/compare/1.4.5...1.4.6
[1.4.5]: https://github.com/infinum/eightshift-ui-components/compare/1.4.4...1.4.5
Expand Down
3 changes: 3 additions & 0 deletions lib/components/draggable-list/draggable-list-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from 'react';

export const DraggableListContext = createContext();
84 changes: 52 additions & 32 deletions lib/components/draggable-list/draggable-list-item.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { GridListItem } from 'react-aria-components';
import { Button } from '../button/button';
import { icons } from '../../icons/icons';
import { clsx } from 'clsx/lite';
import { __ } from '@wordpress/i18n';
import { RichLabel } from '../rich-label/rich-label';
import { DraggableListContext } from './draggable-list-context';
import { useContext } from 'react';

/**
* A DraggableList item.
Expand All @@ -15,7 +14,8 @@ import { RichLabel } from '../rich-label/rich-label';
* @param {string} [props.subtitle] - Subtitle to display.
* @param {JSX.Element|JSX.Element[]} [props.actions] - Actions to display to the right of the label.
* @param {string} [props.textValue] - The text value of the item.
* @param {string} [props.className] - Classes to pass to the item.
* @param {string} [props.className] - Classes to pass to the label.
* @param {string} [props.containerClassName] - Classes to pass to the item container.
*
* @returns {JSX.Element} The DraggableList component.
*
Expand All @@ -24,46 +24,66 @@ import { RichLabel } from '../rich-label/rich-label';
* @preserve
*/
export const DraggableListItem = (props) => {
const { children, icon, label, subtitle, 'aria-label': ariaLabel, className, textValue, ...rest } = props;
const { children, icon, label, subtitle, className, containerClassName, ...rest } = props;

let a11yLabel = textValue;
const { labelAsHandle } = useContext(DraggableListContext);

if (label?.length > 0) {
a11yLabel = label;
}

if (a11yLabel === '' || !a11yLabel) {
a11yLabel = __('New item', 'eightshift-ui-components');
}
const labelElement = (
<RichLabel
hidden={!(icon || label || subtitle)}
icon={icon}
label={label}
subtitle={subtitle}
className={className}
fullWidth
/>
);

return (
<GridListItem
aria-label={ariaLabel ?? a11yLabel}
textValue={a11yLabel}
<div
className={clsx(
'es-uic-flex es-uic-min-h-7 es-uic-items-center es-uic-gap-1 es-uic-rounded-lg es-uic-transition',
'focus:es-uic-outline-none focus-visible:es-uic-ring focus-visible:es-uic-ring-teal-500 focus-visible:es-uic-ring-opacity-50',
containerClassName,
)}
{...rest}
>
<RichLabel
icon={icon}
label={label}
subtitle={subtitle}
className={className}
fullWidth
/>
{labelAsHandle && <div data-swapy-handle>{labelElement}</div>}

{!labelAsHandle && labelElement}

<Button
size='small'
className='es-uic-ml-auto es-uic-h-6 es-uic-w-4 es-uic-shrink-0 !es-uic-text-gray-500 es-uic-opacity-5 focus:!es-uic-opacity-100'
slot='drag'
type='ghost'
icon={icons.reorderGrabberV}
tooltip={__('Re-order', 'eightshift-ui-components')}
/>
{children}
</div>
);
};

/**
* A Draggable item handle.
*
* @component
* @param {Object} props - Component props.
* @param {string} [props.className] - Classes to pass to the handle.
*
* @returns {JSX.Element} The DraggableListItemHandle component.
*
* @example
* <DraggableListItemHandle />
*
* @preserve
*/
export const DraggableListItemHandle = (props) => {
const { className, children, ...rest } = props;

return (
<div
data-swapy-handle
className={
className ??
'es-uic-relative es-uic-h-6 es-uic-w-2 es-uic-cursor-pointer es-uic-items-center es-uic-justify-center es-uic-self-center es-uic-rounded es-uic-border es-uic-border-gray-300 es-uic-bg-gray-50 es-uic-transition after:es-uic-absolute after:es-uic-inset-0 after:es-uic-m-auto after:es-uic-h-4 after:es-uic-w-px after:es-uic-bg-gray-200 after:es-uic-transition after:es-uic-content-[""] hover:es-uic-border-teal-500 hover:es-uic-bg-teal-400 hover:after:es-uic-bg-teal-500'
}
{...rest}
>
{children}
</GridListItem>
</div>
);
};
198 changes: 115 additions & 83 deletions lib/components/draggable-list/draggable-list.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { DropIndicator } from 'react-aria-components';
import { useListData } from 'react-stately';
import { GridList, useDragAndDrop } from 'react-aria-components';
import { useEffect, useId } from 'react';
import { clsx } from 'clsx/lite';
import { useEffect, useId, useRef, useState, useMemo } from 'react';
import { __ } from '@wordpress/i18n';
import { BaseControl } from '../base-control/base-control';
import { createSwapy } from 'swapy';
import { DraggableListContext } from './draggable-list-context';
import { clsx } from 'clsx/lite';

const fixIds = (items, itemIdBase) => {
return items.map((item, i) => ({
...item,
id: item?.id ?? `${itemIdBase}-${i}`,
}));
};

/**
* A component that allows re-ordering a list of items.
Expand All @@ -17,11 +23,11 @@ import { BaseControl } from '../base-control/base-control';
* @param {string} [props.help] - Help text to display below the input.
* @param {JSX.Element|JSX.Element[]} [props.actions] - Actions to display to the right of the label.
* @param {Object<string, any>[]} props.items - Data to display in the list.
* @param {boolean} [props.hideEmptyState] - If `true`, the empty state will not be displayed when there are no items.
* @param {Function} props.onChange - Function to run when the items change.
* @param {boolean} [props.hidden] - If `true`, the component is not rendered.
* @param {boolean} [props.disabled] - If `true`, item reordering is disabled.
* @param {string} [props.className] - Classes to pass to the item wrapper.
* @param {boolean} [props.labelAsHandle=false] - If `true`, the label will be used as the handle for dragging.
*
* @returns {JSX.Element} The DraggableList component.
*
Expand Down Expand Up @@ -57,77 +63,87 @@ export const DraggableList = (props) => {

const {
children,

items: rawItems,
onChange,
items,
'aria-label': ariaLabel,

icon,
label,
subtitle,
help,
actions,
hideEmptyState,

disabled,
className,

labelAsHandle,

hidden,
...rest
} = props;

const list = useListData({
initialItems: items?.map((item, i) => ({ id: item.id ?? `${itemIdBase}${i}`, ...item })),
getKey: ({ id }) => id,
});

let { dragAndDropHooks } = useDragAndDrop({
isDisabled: disabled,
getItems: (keys) => [...keys].map((key) => ({ 'text/plain': list.getItem(key).id })),
onReorder(e) {
if (e.target.dropPosition === 'before') {
list.moveBefore(e.target.key, e.keys);
} else if (e.target.dropPosition === 'after') {
list.moveAfter(e.target.key, e.keys);
}
},
renderDropIndicator(target) {
return (
<DropIndicator
target={target}
className={({ isDropTarget }) =>
clsx(
'es-uic-h-10 es-uic-rounded-lg es-uic-border es-uic-border-gray-300 es-uic-transition',
isDropTarget ? 'es-uic-border-teal-500 es-uic-bg-teal-500/5' : 'es-uic-border-dashed',
)
}
/>
);
},
async onInsert(e) {
let items = await Promise.all(
e.items.map(async (item) => {
let name = item.kind === 'text' ? await item.getText('text/plain') : item.name;

return { id: Math.random(), name };
}),
);

if (e.target.dropPosition === 'before') {
list.insertBefore(e.target.key, ...items);
} else if (e.target.dropPosition === 'after') {
list.insertAfter(e.target.key, ...items);
}
},
});

// Update main value when items change.
const items = useMemo(() => fixIds(rawItems, itemIdBase), [rawItems]);

const ref = useRef(null);
const swapyRef = useRef(null);

const [slotItemsMap, setSlotItemsMap] = useState([
...items.map((item) => ({
slotId: item.id,
itemId: item.id,
})),
{ slotId: `${Math.round(Math.random() * 99999)}`, itemId: null },
]);

const slottedItems = useMemo(
() =>
slotItemsMap.map(({ slotId, itemId }) => ({
slotId,
itemId,
item: items.find((item) => item.id === itemId),
})),
[items, slotItemsMap],
);

// Keep Swapy slots in sync with items.
useEffect(() => {
const items = list.items.map((item) => {
const { id, ...rest } = item;
const newItems = items
.filter((item) => !slotItemsMap.some((slotItem) => slotItem.itemId === item.id))
.map((item) => ({
slotId: item.id,
itemId: item.id,
}));

// Remove items from slotItemsMap if they no longer exist in items
const withoutRemovedItems = slotItemsMap.filter((slotItem) => items.some((item) => item.id === slotItem.itemId) || !slotItem.itemId);

return rest;
const updatedSlotItemsMap = [...withoutRemovedItems, ...newItems];

setSlotItemsMap(updatedSlotItemsMap);
swapyRef.current?.setData({ array: updatedSlotItemsMap });
}, [items]);

// Initialize Swapy.
useEffect(() => {
const container = ref?.current;

swapyRef.current = createSwapy(container, {
manualSwap: true,
});

onChange(items);
}, [list.items]);
swapyRef.current.onSwap(({ data }) => {
const tweakedItems = data.array.filter(({ itemId }) => itemId !== null).map(({ itemId }) => items.find((item) => item?.id === itemId));
onChange(tweakedItems);

// Set data manually.
swapyRef.current?.setData({ array: data.array });
setSlotItemsMap(data.array);
});

return () => {
swapyRef.current?.destroy();
};
}, []);

if (hidden) {
return null;
Expand All @@ -142,30 +158,46 @@ export const DraggableList = (props) => {
actions={actions}
className='es-uic-w-full'
>
<GridList
aria-label={ariaLabel ?? __('Draggable list', 'eightshift-ui-components')}
selectionMode='none'
items={list.items.map((item, index) => ({
...item,
updateData: (newValue) => {
list.update(item.id, { ...list.getItem(item.id), ...newValue });
},
itemIndex: index,
deleteItem: () => list.remove(item.id),
}))}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() =>
hideEmptyState ? null : (
<div className='es-uic-rounded-md es-uic-border es-uic-border-dashed es-uic-border-gray-300 es-uic-p-2 es-uic-text-sm es-uic-text-gray-400'>
{__('No items', 'eightshift-ui-components')}
<DraggableListContext.Provider value={{ labelAsHandle: labelAsHandle }}>
<div
ref={ref}
{...rest}
>
{slottedItems.map(({ itemId, slotId, item }, index) => (
<div
className='es-uic-group es-uic-transition-colors data-[swapy-highlighted]:es-uic-rounded-md data-[swapy-highlighted]:es-uic-outline-dashed data-[swapy-highlighted]:es-uic-outline-1 data-[swapy-highlighted]:es-uic-outline-teal-500/50'
data-swapy-slot={slotId}
key={slotId}
>
{item && (
<div
className={clsx(
'es-uic-transition-[background-color,_box-shadow,_border-radius,_border]',
'group-data-[swapy-highlighted]:es-uic-rounded-md group-data-[swapy-highlighted]:es-uic-bg-white group-data-[swapy-highlighted]:es-uic-shadow',
)}
data-swapy-item={itemId}
key={itemId}
>
{children({
...item,
updateData: (newValue) => {
onChange(items.map((i) => (i.id === itemId ? { ...i, ...newValue } : i)));
},
itemIndex: index,
deleteItem: () => {
onChange(items.filter((i) => i.id !== item.id));

if (item.onAfterItemRemove) {
onAfterItemRemove(item);
}
},
})}
</div>
)}
</div>
)
}
className={className}
{...rest}
>
{children}
</GridList>
))}
</div>
</DraggableListContext.Provider>
</BaseControl>
);
};
Loading

0 comments on commit d72420a

Please sign in to comment.