Skip to content

Commit

Permalink
feat(list): Add support for dragging items. (#7109)
Browse files Browse the repository at this point in the history
**Related Issue:** #6554

## Summary

- SortableComponent
- Moves all configuration options into the interface so a component can
consistently setup SortableJS.
- Changes usage of `onUpdate` to `onSort` for moving between lists and
getting `calciteListOrderChange` event.
- Adds support for `canPut`/`canPull` so users can configure whether an
item can be dragged to another list and vice versa.
- List
  - Sets up sorting
- keyboard sorting only works within a list. Cannot keyboard sort across
lists at this time (including nested lists).
  - Adds dragHandle rendering to `list-item`
- ListItemGroup and List emit an internal event when its default slot
changes in order to update whether an expand caret shows or not.
- Handle
- Updates handle to support displaying ariaLabel (logic taken from
value-list)
- Handle will emit an internal event for parent components to update an
aria-live region.
- No breaking changes necessary. We can advise users to nest another
`calcite-list` to work with sorting on children.
- `calcite-list-item-group` will not be draggable/sortable at this time.
  • Loading branch information
driskull authored Aug 2, 2023
1 parent de97c49 commit 7324f70
Show file tree
Hide file tree
Showing 21 changed files with 920 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"dragHandle": "Drag handle"
"dragHandle": "Drag handle",
"dragHandleActive": "Reordering {itemLabel}, current position {position} of {total}.",
"dragHandleChange": "{itemLabel}, new position {position} of {total}. Press space to confirm.",
"dragHandleCommit": "{itemLabel}, current position {position} of {total}.",
"dragHandleIdle": "{itemLabel}, press space and use arrow keys to reorder content. Current position {position} of {total}."
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"dragHandle": "Drag handle"
"dragHandle": "Drag handle",
"dragHandleActive": "Reordering {itemLabel}, current position {position} of {total}.",
"dragHandleChange": "{itemLabel}, new position {position} of {total}. Press space to confirm.",
"dragHandleCommit": "{itemLabel}, current position {position} of {total}.",
"dragHandleIdle": "{itemLabel}, press space and use arrow keys to reorder content. Current position {position} of {total}."
}
67 changes: 65 additions & 2 deletions packages/calcite-components/src/components/handle/handle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
updateMessages,
} from "../../utils/t9n";
import { HandleMessages } from "./assets/handle/t9n";
import { HandleNudge } from "./interfaces";
import { HandleChange, HandleNudge } from "./interfaces";
import { CSS, ICONS } from "./resources";

@Component({
Expand All @@ -47,6 +47,21 @@ export class Handle implements LoadableComponent, T9nComponent {
*/
@Prop({ mutable: true, reflect: true }) activated = false;

@Watch("messages")
@Watch("label")
@Watch("activated")
@Watch("setPosition")
@Watch("setSize")
handleAriaTextChange(): void {
const message = this.getAriaText("live");

if (message) {
this.calciteInternalHandleChange.emit({
message,
});
}
}

/**
* Value for the button title attribute
*/
Expand All @@ -59,6 +74,27 @@ export class Handle implements LoadableComponent, T9nComponent {
*/
@Prop() messages: HandleMessages;

/**
*
*
* @internal
*/
@Prop() setPosition: number;

/**
*
*
* @internal
*/
@Prop() setSize: number;

/**
*
*
* @internal
*/
@Prop() label: string;

/**
* Use this property to override individual strings used by the component.
*/
Expand Down Expand Up @@ -124,6 +160,11 @@ export class Handle implements LoadableComponent, T9nComponent {
*/
@Event({ cancelable: false }) calciteHandleNudge: EventEmitter<HandleNudge>;

/**
* Emitted when the handle is activated or deactivated.
*/
@Event({ cancelable: false }) calciteInternalHandleChange: EventEmitter<HandleChange>;

// --------------------------------------------------------------------------
//
// Methods
Expand All @@ -144,6 +185,27 @@ export class Handle implements LoadableComponent, T9nComponent {
//
// --------------------------------------------------------------------------

getAriaText(type: "label" | "live"): string {
const { setPosition, setSize, label, messages, activated } = this;

if (!messages || !label || typeof setSize !== "number" || typeof setPosition !== "number") {
return null;
}

const text =
type === "label"
? activated
? messages.dragHandleChange
: messages.dragHandleIdle
: activated
? messages.dragHandleActive
: messages.dragHandleCommit;

const replacePosition = text.replace("{position}", setPosition.toString());
const replaceLabel = replacePosition.replace("{itemLabel}", label);
return replaceLabel.replace("{total}", setSize.toString());
}

handleKeyDown = (event: KeyboardEvent): void => {
switch (event.key) {
case " ":
Expand Down Expand Up @@ -181,13 +243,14 @@ export class Handle implements LoadableComponent, T9nComponent {
return (
// Needs to be a span because of https://github.com/SortableJS/Sortable/issues/1486
<span
aria-label={this.getAriaText("label")}
aria-pressed={toAriaBoolean(this.activated)}
class={{ [CSS.handle]: true, [CSS.handleActivated]: this.activated }}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
role="button"
tabindex="0"
title={this.messages.dragHandle}
title={this.messages?.dragHandle}
// eslint-disable-next-line react/jsx-sort-props
ref={(el): void => {
this.handleButton = el;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export interface HandleNudge {
direction: "up" | "down";
}

export interface HandleChange {
message: string;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Component, Element, h, Host, Prop, State, VNode } from "@stencil/core";
import {
Component,
Element,
Event,
EventEmitter,
h,
Host,
Prop,
State,
VNode,
} from "@stencil/core";
import {
connectInteractive,
disconnectInteractive,
Expand Down Expand Up @@ -34,6 +44,18 @@ export class ListItemGroup implements InteractiveComponent {
*/
@Prop({ reflect: true }) heading: string;

//--------------------------------------------------------------------------
//
// Events
//
//--------------------------------------------------------------------------

/**
* Emitted when the default slot has changes in order to notify parent lists.
*/
@Event({ cancelable: false })
calciteInternalListItemGroupDefaultSlotChange: EventEmitter<DragEvent>;

// --------------------------------------------------------------------------
//
// Lifecycle
Expand Down Expand Up @@ -82,8 +104,18 @@ export class ListItemGroup implements InteractiveComponent {
{heading}
</td>
</tr>
<slot />
<slot onSlotchange={this.handleDefaultSlotChange} />
</Host>
);
}

// --------------------------------------------------------------------------
//
// Private Methods
//
// --------------------------------------------------------------------------

private handleDefaultSlotChange = (): void => {
this.calciteInternalListItemGroupDefaultSlotChange.emit();
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ describe("calcite-list-item", () => {
propertyName: "open",
defaultValue: false,
},
{
propertyName: "dragHandle",
defaultValue: false,
},
]);
});

Expand All @@ -54,6 +58,24 @@ describe("calcite-list-item", () => {
disabled(`<calcite-list-item label="test" active></calcite-list-item>`);
});

it("renders dragHandle when property is true", async () => {
const page = await newE2EPage();
await page.setContent(`<calcite-list-item></calcite-list-item>`);
await page.waitForChanges();

let handleNode = await page.find("calcite-list-item >>> calcite-handle");

expect(handleNode).toBeNull();

const item = await page.find("calcite-list-item");
item.setProperty("dragHandle", true);
await page.waitForChanges();

handleNode = await page.find("calcite-list-item >>> calcite-handle");

expect(handleNode).not.toBeNull();
});

it("renders content node when label is provided", async () => {
const page = await newE2EPage({ html: `<calcite-list-item label="test"></calcite-list-item>` });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ td:focus {
.content-start,
.content-end,
.selection-container,
.drag-container,
.open-container {
@apply flex items-center;
}
Expand Down
49 changes: 46 additions & 3 deletions packages/calcite-components/src/components/list-item/list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EventEmitter,
h,
Host,
Listen,
Method,
Prop,
State,
Expand Down Expand Up @@ -106,6 +107,13 @@ export class ListItem
this.emitCalciteInternalListItemChange();
}

/**
* When `true`, the component displays a draggable button.
*
* @internal
*/
@Prop() dragHandle = false;

/**
* The label text of the component. Displays above the description text.
*/
Expand Down Expand Up @@ -226,6 +234,13 @@ export class ListItem
*/
@Event({ cancelable: false }) calciteInternalListItemChange: EventEmitter<void>;

@Listen("calciteInternalListItemGroupDefaultSlotChange")
@Listen("calciteInternalListDefaultSlotChange")
handleCalciteInternalListDefaultSlotChanges(event: CustomEvent): void {
event.stopPropagation();
this.handleOpenableChange(this.defaultSlotEl);
}

// --------------------------------------------------------------------------
//
// Private Properties
Expand Down Expand Up @@ -269,6 +284,14 @@ export class ListItem

actionsEndEl: HTMLTableCellElement;

defaultSlotEl: HTMLSlotElement;

// --------------------------------------------------------------------------
//
// Lifecycle
//
// --------------------------------------------------------------------------

connectedCallback(): void {
connectInteractive(this);
connectLocalized(this);
Expand Down Expand Up @@ -354,6 +377,14 @@ export class ListItem
);
}

renderDragHandle(): VNode {
return this.dragHandle ? (
<td class={CSS.dragContainer} key="drag-handle-container">
<calcite-handle label={this.label} setPosition={this.setPosition} setSize={this.setSize} />
</td>
) : null;
}

renderOpen(): VNode {
const { el, open, openable, parentListEl } = this;
const dir = getElementDir(el);
Expand Down Expand Up @@ -533,6 +564,7 @@ export class ListItem
// eslint-disable-next-line react/jsx-sort-props
ref={(el) => (this.containerEl = el)}
>
{this.renderDragHandle()}
{this.renderSelected()}
{this.renderOpen()}
{this.renderActionsStart()}
Expand All @@ -545,7 +577,10 @@ export class ListItem
[CSS.nestedContainerHidden]: openable && !open,
}}
>
<slot onSlotchange={this.handleDefaultSlotChange} />
<slot
onSlotchange={this.handleDefaultSlotChange}
ref={(el: HTMLSlotElement) => (this.defaultSlotEl = el)}
/>
</div>
</Host>
);
Expand Down Expand Up @@ -602,9 +637,13 @@ export class ListItem
}
}

handleDefaultSlotChange = (event: Event): void => {
handleOpenableChange(slotEl: HTMLSlotElement): void {
if (!slotEl) {
return;
}

const { parentListEl } = this;
const listItemChildren = getListItemChildren(event);
const listItemChildren = getListItemChildren(slotEl);
updateListItemChildren(listItemChildren);
const openable = !!listItemChildren.length;

Expand All @@ -617,6 +656,10 @@ export class ListItem
if (!openable) {
this.open = false;
}
}

handleDefaultSlotChange = (event: Event): void => {
this.handleOpenableChange(event.target as HTMLSlotElement);
};

toggleOpen = (): void => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const CSS = {
actionsEnd: "actions-end",
selectionContainer: "selection-container",
openContainer: "open-container",
dragContainer: "drag-container",
};

export const SLOTS = {
Expand All @@ -27,7 +28,8 @@ export const SLOTS = {
actionsEnd: "actions-end",
};

export const MAX_COLUMNS = 5;
// Set to zero to extend until the end of the table section.
export const MAX_COLUMNS = 0;

export const ICONS = {
selectedMultiple: "check-circle-f",
Expand Down
11 changes: 8 additions & 3 deletions packages/calcite-components/src/components/list-item/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Build } from "@stencil/core";

const listSelector = "calcite-list";
const listItemGroupSelector = "calcite-list-item-group";
const listItemSelector = "calcite-list-item";

export function getListItemChildren(event: Event): HTMLCalciteListItemElement[] {
const assignedElements = (event.target as HTMLSlotElement).assignedElements({ flatten: true });
export function getListItemChildren(slotEl: HTMLSlotElement): HTMLCalciteListItemElement[] {
const assignedElements = slotEl.assignedElements({ flatten: true });

const listItemGroupChildren = (
assignedElements.filter((el) => el?.matches(listItemGroupSelector)) as HTMLCalciteListItemGroupElement[]
Expand All @@ -16,7 +17,11 @@ export function getListItemChildren(event: Event): HTMLCalciteListItemElement[]
el?.matches(listItemSelector)
) as HTMLCalciteListItemElement[];

return [...listItemGroupChildren, ...listItemChildren];
const listItemListChildren = (assignedElements.filter((el) => el?.matches(listSelector)) as HTMLCalciteListElement[])
.map((list) => Array.from(list.querySelectorAll(listItemSelector)))
.reduce((previousValue, currentValue) => [...previousValue, ...currentValue], []);

return [...listItemListChildren, ...listItemGroupChildren, ...listItemChildren];
}

export function updateListItemChildren(listItemChildren: HTMLCalciteListItemElement[]): void {
Expand Down
Loading

0 comments on commit 7324f70

Please sign in to comment.