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

feat(list): Add support for dragging items. #7109

Merged
merged 49 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
59f1968
WIP
driskull Jun 3, 2023
ceea051
feat(list): Add support for sorting and dragging items. #6554
driskull Jun 3, 2023
0478bb0
cleanup
driskull Jun 3, 2023
84cd376
Merge branch 'master' into dris0000/sortable-list
driskull Jun 5, 2023
49ce1f8
Merge branch 'master' into dris0000/sortable-list
driskull Jun 5, 2023
ae85ebc
Merge branch 'master' into dris0000/sortable-list
driskull Jun 6, 2023
5bcd803
Merge branch 'dris0000/sortable-list' of github.com:Esri/calcite-comp…
driskull Jun 6, 2023
2eae6be
Merge branch 'master' into dris0000/sortable-list
driskull Jun 6, 2023
befbfea
cleanup
driskull Jun 7, 2023
bf8ecad
add pull/put events
driskull Jun 7, 2023
84502ef
feat(list): Add support for sorting and dragging items. #6554
driskull Jun 7, 2023
88a9258
add comment
driskull Jun 7, 2023
9a7b80d
cleanup
driskull Jun 7, 2023
5ef19b2
cleanup
driskull Jun 7, 2023
de11b60
Merge branch 'master' into dris0000/sortable-list
driskull Jun 7, 2023
ccc748e
drag handle alignment
driskull Jun 7, 2023
8ab64fe
cleanup
driskull Jun 8, 2023
0cacf88
Merge branch 'master' into dris0000/sortable-list
driskull Jun 15, 2023
a2acfdb
wip
driskull Jun 15, 2023
ccf0a9a
Merge branch 'master' into dris0000/sortable-list
driskull Jun 28, 2023
628c09f
Merge branch 'main' into dris0000/sortable-list
driskull Jul 3, 2023
a553935
Merge branch 'main' into dris0000/sortable-list
driskull Jul 3, 2023
9b6dd24
cleanup
driskull Jul 3, 2023
f010af2
cleanup
driskull Jul 3, 2023
24e20e9
Merge branch 'main' into dris0000/sortable-list
driskull Jul 3, 2023
1c922ba
Merge branch 'main' into dris0000/sortable-list
driskull Jul 11, 2023
d0eae6a
Merge branch 'main' into dris0000/sortable-list
driskull Jul 14, 2023
19e9eff
event detail
driskull Jul 14, 2023
d06d16f
cleanup
driskull Jul 14, 2023
980a144
cleanup
driskull Jul 14, 2023
9b6c5eb
test app
driskull Jul 17, 2023
e4943b0
Merge branch 'main' into dris0000/sortable-list
driskull Jul 17, 2023
69c3138
Merge branch 'main' into dris0000/sortable-list
driskull Jul 24, 2023
bbf9a68
Merge branch 'main' into dris0000/sortable-list
driskull Jul 27, 2023
d20bea8
cleanup slot changes for openable
driskull Jul 27, 2023
99e5e12
WIP sorting keyboard
driskull Jul 27, 2023
f97ceea
WIP
driskull Jul 27, 2023
3580920
WIP
driskull Jul 27, 2023
f1f94c7
cleanup
driskull Jul 27, 2023
202b9e7
Merge branch 'main' into dris0000/sortable-list
driskull Jul 27, 2023
ca77444
cleanup
driskull Jul 28, 2023
83fb4c0
cleanup
driskull Jul 28, 2023
98acf7d
cleanup
driskull Jul 28, 2023
b7eb538
cleanup
driskull Jul 28, 2023
4b4329a
Merge branch 'main' into dris0000/sortable-list
driskull Jul 28, 2023
e174a94
examples, tests
driskull Jul 28, 2023
162cd5e
cleanup
driskull Jul 28, 2023
f820923
fix test
driskull Aug 1, 2023
f826ce0
review fixes
driskull Aug 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,20 @@ 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();

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

const contentNode = await page.find("calcite-list-item >>> calcite-handle");
driskull marked this conversation as resolved.
Show resolved Hide resolved

expect(contentNode).not.toBeNull();
driskull marked this conversation as resolved.
Show resolved Hide resolved
});

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)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this arrow function need to be called each render?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its only called once when the ref is established

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just seen patterns where we declare a private method on the class for assigning the ref so that the function doesn't have to get recalled on every render, so that stood out to me.

/>
</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