diff --git a/app/assets/images/lookbook/drag_and_drop_anatomy.png b/app/assets/images/lookbook/drag_and_drop_anatomy.png new file mode 100644 index 000000000000..69e95ef5dfe6 Binary files /dev/null and b/app/assets/images/lookbook/drag_and_drop_anatomy.png differ diff --git a/app/assets/images/lookbook/sortable_lists_a.png b/app/assets/images/lookbook/sortable_lists_a.png new file mode 100644 index 000000000000..7bd230173ffe Binary files /dev/null and b/app/assets/images/lookbook/sortable_lists_a.png differ diff --git a/app/assets/images/lookbook/sortable_lists_b.png b/app/assets/images/lookbook/sortable_lists_b.png new file mode 100644 index 000000000000..600123160902 Binary files /dev/null and b/app/assets/images/lookbook/sortable_lists_b.png differ diff --git a/app/assets/images/lookbook/sortable_lists_c.png b/app/assets/images/lookbook/sortable_lists_c.png new file mode 100644 index 000000000000..4c3f7b29333c Binary files /dev/null and b/app/assets/images/lookbook/sortable_lists_c.png differ diff --git a/app/assets/images/lookbook/sortable_lists_d.png b/app/assets/images/lookbook/sortable_lists_d.png new file mode 100644 index 000000000000..b091b3b6242a Binary files /dev/null and b/app/assets/images/lookbook/sortable_lists_d.png differ diff --git a/frontend/src/stimulus/controllers/dynamic/admin/type-form-configuration/drag-and-drop.controller.ts b/frontend/src/stimulus/controllers/dynamic/admin/type-form-configuration/drag-and-drop.controller.ts index 6e3c08b89ed1..fcdae7d77d8c 100644 --- a/frontend/src/stimulus/controllers/dynamic/admin/type-form-configuration/drag-and-drop.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/admin/type-form-configuration/drag-and-drop.controller.ts @@ -28,6 +28,10 @@ import GenericDragAndDropController from '../../generic-drag-and-drop.controller'; +/** + * @deprecated Inherits the deprecated {@link GenericDragAndDropController}; + * migrate this surface to the `sortable-lists` controller suite. + */ export default class extends GenericDragAndDropController { protected buildData(el:Element, target:Element):FormData { const data = super.buildData(el, target); diff --git a/frontend/src/stimulus/controllers/dynamic/generic-drag-and-drop.controller.ts b/frontend/src/stimulus/controllers/dynamic/generic-drag-and-drop.controller.ts index 625a90b3042f..d4964aa69f67 100644 --- a/frontend/src/stimulus/controllers/dynamic/generic-drag-and-drop.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/generic-drag-and-drop.controller.ts @@ -42,6 +42,13 @@ interface TargetConfig { targetId:string|null; } +/** + * @deprecated Closed to new consumers. Use the `sortable-lists` controller + * suite instead — or, once it lands, the declarative `BorderBoxListComponent` + * options that emit its contract. This controller is Dragula-based, sends + * index-based positions, and offers no keyboard path; its remaining call sites + * are being migrated. See the drag and drop pattern page in Lookbook. + */ export default class GenericDragAndDropController extends Controller { static targets = ['container', 'scrollContainer']; diff --git a/frontend/src/stimulus/controllers/dynamic/meetings/drag-and-drop.controller.ts b/frontend/src/stimulus/controllers/dynamic/meetings/drag-and-drop.controller.ts index fe61455331be..c15ef22ad925 100644 --- a/frontend/src/stimulus/controllers/dynamic/meetings/drag-and-drop.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/meetings/drag-and-drop.controller.ts @@ -30,6 +30,10 @@ import GenericDragAndDropController from '../generic-drag-and-drop.controller'; import { appendCollapsedState } from '../../../helpers/meetings-helpers'; import { hasUnsavedChanges } from '../../../helpers/meetings-helpers'; +/** + * @deprecated Inherits the deprecated {@link GenericDragAndDropController}; + * migrate this surface to the `sortable-lists` controller suite. + */ export default class extends GenericDragAndDropController { protected override buildData(el:Element, target:Element):FormData { const data = super.buildData(el, target); diff --git a/lookbook/docs/01-how-to-use.md.erb b/lookbook/docs/01-how-to-use.md.erb index d9fd9d10d0b6..3cfc7b554556 100644 --- a/lookbook/docs/01-how-to-use.md.erb +++ b/lookbook/docs/01-how-to-use.md.erb @@ -17,9 +17,13 @@ If you are looking for a specific object you know by name, you can also use the ### Patterns -In the lower left, there is a section called "Patterns". In here, we will document specific decisions and patterns concerning how we approach the use of certain components, or how certain things (like dialogs or flash banners) work in OpenProject. +In the lower left, there is a section called "Patterns". In here, we will document specific decisions and patterns concerning how we approach the use of certain components, or how certain things (like dialogs or flash banners) work in OpenProject. -This includes behaviour and patters that explicitly do not follow the Primer approach, as well as specific UX patterns that are unique to OpenProject. +This includes behaviour and patterns that explicitly do not follow the Primer approach, as well as specific UX patterns that are unique to OpenProject. + +Some patterns also include implementation guidance and live previews. The +[Drag and drop](./patterns/drag_and_drop) page has tabs for design guidance, +implementation scenarios and an API reference. ### Icons diff --git a/lookbook/docs/components/border-box-list.md.erb b/lookbook/docs/10-components/border-box-list.md.erb similarity index 100% rename from lookbook/docs/components/border-box-list.md.erb rename to lookbook/docs/10-components/border-box-list.md.erb diff --git a/lookbook/docs/components/check-all.md.erb b/lookbook/docs/10-components/check-all.md.erb similarity index 100% rename from lookbook/docs/components/check-all.md.erb rename to lookbook/docs/10-components/check-all.md.erb diff --git a/lookbook/docs/components/collapsible-section.md.erb b/lookbook/docs/10-components/collapsible-section.md.erb similarity index 100% rename from lookbook/docs/components/collapsible-section.md.erb rename to lookbook/docs/10-components/collapsible-section.md.erb diff --git a/lookbook/docs/components/dialogs/danger-dialog.md.erb b/lookbook/docs/10-components/dialogs/danger-dialog.md.erb similarity index 100% rename from lookbook/docs/components/dialogs/danger-dialog.md.erb rename to lookbook/docs/10-components/dialogs/danger-dialog.md.erb diff --git a/lookbook/docs/components/dialogs/feedback-dialog-and-feedback-message.md.erb b/lookbook/docs/10-components/dialogs/feedback-dialog-and-feedback-message.md.erb similarity index 100% rename from lookbook/docs/components/dialogs/feedback-dialog-and-feedback-message.md.erb rename to lookbook/docs/10-components/dialogs/feedback-dialog-and-feedback-message.md.erb diff --git a/lookbook/docs/components/dialogs/full-page-prompt.md.erb b/lookbook/docs/10-components/dialogs/full-page-prompt.md.erb similarity index 100% rename from lookbook/docs/components/dialogs/full-page-prompt.md.erb rename to lookbook/docs/10-components/dialogs/full-page-prompt.md.erb diff --git a/lookbook/docs/components/email-updates-mode-selector.md.erb b/lookbook/docs/10-components/email-updates-mode-selector.md.erb similarity index 100% rename from lookbook/docs/components/email-updates-mode-selector.md.erb rename to lookbook/docs/10-components/email-updates-mode-selector.md.erb diff --git a/lookbook/docs/components/enterprise.md.erb b/lookbook/docs/10-components/enterprise.md.erb similarity index 100% rename from lookbook/docs/components/enterprise.md.erb rename to lookbook/docs/10-components/enterprise.md.erb diff --git a/lookbook/docs/components/expandable-text.md.erb b/lookbook/docs/10-components/expandable-text.md.erb similarity index 100% rename from lookbook/docs/components/expandable-text.md.erb rename to lookbook/docs/10-components/expandable-text.md.erb diff --git a/lookbook/docs/components/headers/page-header.md.erb b/lookbook/docs/10-components/headers/page-header.md.erb similarity index 100% rename from lookbook/docs/components/headers/page-header.md.erb rename to lookbook/docs/10-components/headers/page-header.md.erb diff --git a/lookbook/docs/components/headers/page-heading.md.erb b/lookbook/docs/10-components/headers/page-heading.md.erb similarity index 100% rename from lookbook/docs/components/headers/page-heading.md.erb rename to lookbook/docs/10-components/headers/page-heading.md.erb diff --git a/lookbook/docs/components/headers/sub-header.md.erb b/lookbook/docs/10-components/headers/sub-header.md.erb similarity index 100% rename from lookbook/docs/components/headers/sub-header.md.erb rename to lookbook/docs/10-components/headers/sub-header.md.erb diff --git a/lookbook/docs/components/hover-cards.md.erb b/lookbook/docs/10-components/hover-cards.md.erb similarity index 100% rename from lookbook/docs/components/hover-cards.md.erb rename to lookbook/docs/10-components/hover-cards.md.erb diff --git a/lookbook/docs/components/inset-box.md.erb b/lookbook/docs/10-components/inset-box.md.erb similarity index 100% rename from lookbook/docs/components/inset-box.md.erb rename to lookbook/docs/10-components/inset-box.md.erb diff --git a/lookbook/docs/components/loading-indicators.md.erb b/lookbook/docs/10-components/loading-indicators.md.erb similarity index 100% rename from lookbook/docs/components/loading-indicators.md.erb rename to lookbook/docs/10-components/loading-indicators.md.erb diff --git a/lookbook/docs/components/quick-filters.md.erb b/lookbook/docs/10-components/quick-filters.md.erb similarity index 100% rename from lookbook/docs/components/quick-filters.md.erb rename to lookbook/docs/10-components/quick-filters.md.erb diff --git a/lookbook/docs/components/select-panel.md.erb b/lookbook/docs/10-components/select-panel.md.erb similarity index 100% rename from lookbook/docs/components/select-panel.md.erb rename to lookbook/docs/10-components/select-panel.md.erb diff --git a/lookbook/docs/components/side-panel.md.erb b/lookbook/docs/10-components/side-panel.md.erb similarity index 100% rename from lookbook/docs/components/side-panel.md.erb rename to lookbook/docs/10-components/side-panel.md.erb diff --git a/lookbook/docs/components/status_button.md.erb b/lookbook/docs/10-components/status_button.md.erb similarity index 100% rename from lookbook/docs/components/status_button.md.erb rename to lookbook/docs/10-components/status_button.md.erb diff --git a/lookbook/docs/components/tables/border-box-table.md.erb b/lookbook/docs/10-components/tables/border-box-table.md.erb similarity index 100% rename from lookbook/docs/components/tables/border-box-table.md.erb rename to lookbook/docs/10-components/tables/border-box-table.md.erb diff --git a/lookbook/docs/components/tables/tables.md.erb b/lookbook/docs/10-components/tables/tables.md.erb similarity index 100% rename from lookbook/docs/components/tables/tables.md.erb rename to lookbook/docs/10-components/tables/tables.md.erb diff --git a/lookbook/docs/components/tree-view/filterable-tree-view.md.erb b/lookbook/docs/10-components/tree-view/filterable-tree-view.md.erb similarity index 100% rename from lookbook/docs/components/tree-view/filterable-tree-view.md.erb rename to lookbook/docs/10-components/tree-view/filterable-tree-view.md.erb diff --git a/lookbook/docs/components/tree-view/tree-view.md.erb b/lookbook/docs/10-components/tree-view/tree-view.md.erb similarity index 100% rename from lookbook/docs/components/tree-view/tree-view.md.erb rename to lookbook/docs/10-components/tree-view/tree-view.md.erb diff --git a/lookbook/docs/components/work-packages/card.md.erb b/lookbook/docs/10-components/work-packages/card.md.erb similarity index 100% rename from lookbook/docs/components/work-packages/card.md.erb rename to lookbook/docs/10-components/work-packages/card.md.erb diff --git a/lookbook/docs/components/work-packages/work-package-info-line.md.erb b/lookbook/docs/10-components/work-packages/work-package-info-line.md.erb similarity index 100% rename from lookbook/docs/components/work-packages/work-package-info-line.md.erb rename to lookbook/docs/10-components/work-packages/work-package-info-line.md.erb diff --git a/lookbook/docs/components/work-packages/work-package-split-view.md.erb b/lookbook/docs/10-components/work-packages/work-package-split-view.md.erb similarity index 100% rename from lookbook/docs/components/work-packages/work-package-split-view.md.erb rename to lookbook/docs/10-components/work-packages/work-package-split-view.md.erb diff --git a/lookbook/docs/patterns/01-layout.md.erb b/lookbook/docs/20-patterns/01-layout.md.erb similarity index 100% rename from lookbook/docs/patterns/01-layout.md.erb rename to lookbook/docs/20-patterns/01-layout.md.erb diff --git a/lookbook/docs/patterns/02-forms/01-index.md.erb b/lookbook/docs/20-patterns/02-forms/01-index.md.erb similarity index 100% rename from lookbook/docs/patterns/02-forms/01-index.md.erb rename to lookbook/docs/20-patterns/02-forms/01-index.md.erb diff --git a/lookbook/docs/patterns/02-forms/02-usage.md.erb b/lookbook/docs/20-patterns/02-forms/02-usage.md.erb similarity index 100% rename from lookbook/docs/patterns/02-forms/02-usage.md.erb rename to lookbook/docs/20-patterns/02-forms/02-usage.md.erb diff --git a/lookbook/docs/patterns/02-forms/03-interactivity.md.erb b/lookbook/docs/20-patterns/02-forms/03-interactivity.md.erb similarity index 100% rename from lookbook/docs/patterns/02-forms/03-interactivity.md.erb rename to lookbook/docs/20-patterns/02-forms/03-interactivity.md.erb diff --git a/lookbook/docs/patterns/02-forms/04-admin-settings.md.erb b/lookbook/docs/20-patterns/02-forms/04-admin-settings.md.erb similarity index 100% rename from lookbook/docs/patterns/02-forms/04-admin-settings.md.erb rename to lookbook/docs/20-patterns/02-forms/04-admin-settings.md.erb diff --git a/lookbook/docs/patterns/02-forms/05-rich-text-editor.md.erb b/lookbook/docs/20-patterns/02-forms/05-rich-text-editor.md.erb similarity index 100% rename from lookbook/docs/patterns/02-forms/05-rich-text-editor.md.erb rename to lookbook/docs/20-patterns/02-forms/05-rich-text-editor.md.erb diff --git a/lookbook/docs/patterns/02-forms/06-best-practices.md.erb b/lookbook/docs/20-patterns/02-forms/06-best-practices.md.erb similarity index 100% rename from lookbook/docs/patterns/02-forms/06-best-practices.md.erb rename to lookbook/docs/20-patterns/02-forms/06-best-practices.md.erb diff --git a/lookbook/docs/patterns/03-autocompleters.md.erb b/lookbook/docs/20-patterns/03-autocompleters.md.erb similarity index 100% rename from lookbook/docs/patterns/03-autocompleters.md.erb rename to lookbook/docs/20-patterns/03-autocompleters.md.erb diff --git a/lookbook/docs/patterns/04-links.md.erb b/lookbook/docs/20-patterns/04-links.md.erb similarity index 100% rename from lookbook/docs/patterns/04-links.md.erb rename to lookbook/docs/20-patterns/04-links.md.erb diff --git a/lookbook/docs/patterns/05-dialogs.md.erb b/lookbook/docs/20-patterns/05-dialogs.md.erb similarity index 100% rename from lookbook/docs/patterns/05-dialogs.md.erb rename to lookbook/docs/20-patterns/05-dialogs.md.erb diff --git a/lookbook/docs/patterns/06-inplace-edit-fields.md.erb b/lookbook/docs/20-patterns/06-inplace-edit-fields.md.erb similarity index 100% rename from lookbook/docs/patterns/06-inplace-edit-fields.md.erb rename to lookbook/docs/20-patterns/06-inplace-edit-fields.md.erb diff --git a/lookbook/docs/patterns/07-menu-and-navigation.md.erb b/lookbook/docs/20-patterns/07-menu-and-navigation.md.erb similarity index 100% rename from lookbook/docs/patterns/07-menu-and-navigation.md.erb rename to lookbook/docs/20-patterns/07-menu-and-navigation.md.erb diff --git a/lookbook/docs/patterns/08-flash-banner.md.erb b/lookbook/docs/20-patterns/08-flash-banner.md.erb similarity index 100% rename from lookbook/docs/patterns/08-flash-banner.md.erb rename to lookbook/docs/20-patterns/08-flash-banner.md.erb diff --git a/lookbook/docs/patterns/09-flash-modal.md.erb b/lookbook/docs/20-patterns/09-flash-modal.md.erb similarity index 100% rename from lookbook/docs/patterns/09-flash-modal.md.erb rename to lookbook/docs/20-patterns/09-flash-modal.md.erb diff --git a/lookbook/docs/patterns/10-contextual-action-menu.md.erb b/lookbook/docs/20-patterns/10-contextual-action-menu.md.erb similarity index 100% rename from lookbook/docs/patterns/10-contextual-action-menu.md.erb rename to lookbook/docs/20-patterns/10-contextual-action-menu.md.erb diff --git a/lookbook/docs/patterns/11-filter-forms.md.erb b/lookbook/docs/20-patterns/11-filter-forms.md.erb similarity index 100% rename from lookbook/docs/patterns/11-filter-forms.md.erb rename to lookbook/docs/20-patterns/11-filter-forms.md.erb diff --git a/lookbook/docs/patterns/17-settings-pages.md.erb b/lookbook/docs/20-patterns/17-settings-pages.md.erb similarity index 100% rename from lookbook/docs/patterns/17-settings-pages.md.erb rename to lookbook/docs/20-patterns/17-settings-pages.md.erb diff --git a/lookbook/docs/patterns/20-attribute-help-texts.md.erb b/lookbook/docs/20-patterns/20-attribute-help-texts.md.erb similarity index 100% rename from lookbook/docs/patterns/20-attribute-help-texts.md.erb rename to lookbook/docs/20-patterns/20-attribute-help-texts.md.erb diff --git a/lookbook/docs/patterns/30-step-processes.md.erb b/lookbook/docs/20-patterns/30-step-processes.md.erb similarity index 100% rename from lookbook/docs/patterns/30-step-processes.md.erb rename to lookbook/docs/20-patterns/30-step-processes.md.erb diff --git a/lookbook/docs/20-patterns/40-drag-and-drop.md.erb b/lookbook/docs/20-patterns/40-drag-and-drop.md.erb new file mode 100644 index 000000000000..0cbd6c5372d6 --- /dev/null +++ b/lookbook/docs/20-patterns/40-drag-and-drop.md.erb @@ -0,0 +1,9 @@ +Drag and drop lets users reorder rows and move them between lists. This +page brings together the design guidance and the implementation of the +`sortable-lists` controller suite. + +Start with **Design guidance** for interaction decisions. To implement drag +and drop, choose **Implementation overview** in the [tabs below](#page-tabbed-sections), +then **One list**, **Sibling lists** or **Nested lists** for your scenario. +**API reference** covers the attributes, request and response contract, and +migration from `generic-drag-and-drop`. diff --git a/lookbook/docs/20-patterns/41-drag-and-drop[design-guidance].md.erb b/lookbook/docs/20-patterns/41-drag-and-drop[design-guidance].md.erb new file mode 100644 index 000000000000..5964ee5b54c1 --- /dev/null +++ b/lookbook/docs/20-patterns/41-drag-and-drop[design-guidance].md.erb @@ -0,0 +1,74 @@ +--- +label: Design guidance +--- + +Drag and drop lets a user change the order of rows, or move a row into a +neighboring list, by picking it up and releasing it where it belongs. In +OpenProject it is always paired with a move menu, so the same change is +reachable from the keyboard and with a screen reader. For implementation +details, choose **Implementation overview** in the [tabs above](#page-tabbed-sections), +then the scenario that matches your list. + +## Anatomy + +" alt="A list of four rows with drag handles; the second row is dimmed while its preview floats beside the list and a line between the third and fourth rows marks where it will land" width="636" style="max-width: 100%"> + +1. **Drag handle**: the grip at the start of a list row. A drag starts here, + nowhere else on the row. A card has no grip: the whole card is its handle. +2. **Drag source**: the row being moved. It stays in place, dimmed, until the + drop. +3. **Drag preview**: the copy of the row that follows the pointer. +4. **Drop indicator**: the line marking where the row will land. It marks a + spot without reserving space. +5. **Drop container**: the outline on the list that will receive the row. + +## When to use it + +Use drag and drop when the order of a list is itself the setting: the +position of an enumeration value, the sequence of sections on a form, the +position of work packages in a sprint. Do not add it to lists whose order +is derived (sorted by name or date) or where a single row moves rarely; a +move menu alone serves those. + +## Where it is used today + +* Admin lists with a persisted order: enumerations, text transform actions, + project phase definitions, export templates, work package types. +* Project Attributes settings: sections reorder, and custom fields reorder + within a section or move to another. +* Backlogs: work packages reorder within the inbox and sprints and move + between them. + +## What users can expect + +* **A visible drag handle** at the start of a list row. The row is picked up + from the handle, not from anywhere on it, so links and buttons inside the + row keep working. A card (Backlogs) shows no grip and is picked up from + anywhere on it except its links and buttons. +* **A drop indicator** that marks where the row will land, and a highlight + on the list that will receive it. +* **A move menu** in the row's action menu ("Move to top", "Move up", "Move + down", "Move to bottom") offering only the directions that apply. +* **An announcement** for screen-reader users when a row lands in another + list, naming that list. +* **Rows that cannot move** still show a position and can be dropped next + to; they simply refuse to be picked up, or refuse to leave their list. +* **Selecting several rows and moving them together** is being added for + Backlogs ([AGILE-278](https://community.openproject.org/wp/AGILE-278)). + +## Vocabulary + +Design and review discussions use one set of terms: *drag source* (the +original row), *drag preview* (what follows the pointer), *drop indicator* +(the line marking the landing spot), *drop container* (the highlighted +receiving list), *reorder* (within a list) and *move* (between lists). +Avoid "mirror", "placeholder" and "container" for these; they meant other +things in the previous implementation. + +## Further reading + +The implementation is built on Atlassian's Pragmatic drag and drop, whose +design guidance applies here too: + +* [Web platform design constraints](https://atlassian.design/components/pragmatic-drag-and-drop/web-platform-design-constraints) +* [Accessibility guidelines](https://atlassian.design/components/pragmatic-drag-and-drop/accessibility-guidelines) diff --git a/lookbook/docs/20-patterns/42-drag-and-drop[implementation-overview].md.erb b/lookbook/docs/20-patterns/42-drag-and-drop[implementation-overview].md.erb new file mode 100644 index 000000000000..b70bbf61b52f --- /dev/null +++ b/lookbook/docs/20-patterns/42-drag-and-drop[implementation-overview].md.erb @@ -0,0 +1,105 @@ +--- +label: Implementation overview +--- + +Server-rendered lists become sortable through the `sortable-lists` Stimulus +controller suite. A drag or a move menu action sends one request describing +where the row landed; the server persists the order and streams the list +back. The same suite drives a single admin list, the Backlogs board and the +nested Project Attributes settings. + +This page documents the markup and HTTP contract for wiring the controllers +by hand. The **Design guidance** tab covers when drag and drop is appropriate +and what users expect from it. + +## Before you wire anything + +**Prefer the component API where it exists.** `BorderBoxListComponent` is +gaining `sortable_list:` / `sortable_item:` options that emit this contract +for you ([DREAM-805](https://community.openproject.org/wp/DREAM-805)). Once +those land they cover a single list and sibling lists of rows the server can +reorder. Wire by hand only for what they do not reach: several item types +under one root, per-type move endpoints, non-`BorderBoxList` markup such as +table rows or cards, or optimistic placement. + +**Do not use `generic-drag-and-drop`.** The Dragula-based controller is +deprecated and closed to new consumers. The API reference tab has a migration +table for its remaining call sites. + +## Anatomy + +Four controllers divide the work and find each other through Stimulus +outlets, never by walking the DOM: + +| Controller | Placed on | Owns | +| --- | --- | --- | +| `sortable-lists` | one wrapper per page region | the move URL, the request, rollback on failure, the outlet selectors for the three children | +| `sortable-lists--list` | each list | the list's type, its id, which item type it accepts, where a drop on empty space lands | +| `sortable-lists--item` | each row that can move | the row's id, type and accessible label; the drag handle and preview; the move menu actions | +| `sortable-lists--scrollable` | whatever element scrolls | auto-scrolling while a drag hovers its edge | + +### A single list + +" alt="One root, one list, three rows; the root reaches the list and each row through outlets" width="460" style="max-width: 100%"> + +The minimum topology. Three controllers; the root reaches the other two +through outlets, never by traversing the DOM. + +### Sibling lists in scrolling columns + +" alt="Two scroll columns under one root, each holding lists of the same type; the scrollable sits between root and lists" width="636" style="max-width: 100%"> + +Two scrolling columns, each holding several lists of one type. The +scrollables sit inside the root and are reached by outlet like every other +child; the root still addresses every item directly, however deep it sits. + +### How the root reaches its children + +" alt="The root's three outlet selectors resolving to the list, item and scrollable controllers" width="760" style="max-width: 100%"> + +Nesting is DOM structure; this is how the controllers address each other. +The root declares one outlet per child type, so it holds a live collection +of each with no `closest()` and no `querySelector` walking. The selectors +are ordinary descendant selectors scoped to the root's own wrapper id, so +two roots on one page cannot capture each other's children: + +```html +
+``` + +Because the selectors match at any depth, one root holds every list and +every item beneath it in one flat collection. A row's owning list is the +innermost list element that contains it. That single rule is what lets one +root serve nested lists as well as flat ones. + +### Vocabulary + +* **Sortable list, sortable item, sortable item type**: a list whose order the + server persists, a row that takes part in that order, and the category a + list accepts. A row is an item once it carries + `data-sortable-lists--item-id-value`; that is what counts it and anchors + its neighbors' drops. The `sortable-lists--item` controller on it is what + makes it draggable and gives it a move menu, and `confined` keeps a + draggable item in its own list. +* **Drag source**: the original row while it is being dragged. **Drag + preview**: the copy that follows the pointer. +* **Drop indicator**: the line marking where the row will land, drawn without + reserving space. **Drop container**: the list-level outline shown while a + compatible row hovers a list (`data-drop-container="active"`). +* **Reorder** happens within one list; a **move** changes list. In terms of + the backend request they are identical. + +## Choosing a scenario + +* **One list**: rows reorder within a single list, such as admin enumerations + or text transform actions. +* **Sibling lists**: several lists of the same kind, with rows moving between + them, as in Backlogs. +* **Nested lists**: rows that are themselves lists, such as Project Attributes + sections and their custom fields. +* **API reference**: every attribute, the backend request, the server half, the + Angular counterpart, migration. diff --git a/lookbook/docs/20-patterns/43-drag-and-drop[one-list].md.erb b/lookbook/docs/20-patterns/43-drag-and-drop[one-list].md.erb new file mode 100644 index 000000000000..bdc3080dd8fe --- /dev/null +++ b/lookbook/docs/20-patterns/43-drag-and-drop[one-list].md.erb @@ -0,0 +1,150 @@ +--- +label: One list +--- + +One list whose rows the server can reorder. `Admin::TextTransformActions` is +the smallest complete implementation in the codebase; the samples below +follow it. + +<%= embed Patterns::SortableListsPreview, :single_list, panels: %i[source] %> + +The preview's move URL points at a path that does not exist, so a drop moves +the row, the request fails, and the row rolls back with an error toast. That +is the whole failure path; persistence needs the endpoint described at the +end of this tab. + +## The root + +One wrapper per page region. It carries the move URL template and the outlet +selectors, scoped to its own id: + +```html +
+``` + +Build the template from the route helper through a sentinel, so an +installation under a relative URL root keeps working; `{id}` is expanded on +the client with the dragged row's id: + +```ruby +def move_url_template + id_placeholder = "__id__" + drop_admin_text_transform_action_path(id_placeholder).sub(id_placeholder, "{id}") +end +``` + +The expanded URL must stay same-origin and relative; only path, query and +fragment survive. + +## The list + +The list element declares the type it persists and the type it accepts. +`type` doubles as the `list_type` the server receives, so a list without one +cannot accept drops and warns in the console. `name` is what screen readers +hear when a row arrives from another list; give it even for one list, it +costs nothing and the list may gain siblings. + +```html +
"> +
    + …rows… +
+
+``` + +Rows may sit in a child `