diff --git a/docs/app/views/examples/components/list/_preview.html.erb b/docs/app/views/examples/components/list/_preview.html.erb index a1aa0932bd..fd9460c9f7 100644 --- a/docs/app/views/examples/components/list/_preview.html.erb +++ b/docs/app/views/examples/components/list/_preview.html.erb @@ -55,41 +55,76 @@ sample_products = [

Default configuration

<%= md(" -SageList is implemented most simply by passing the desired contents for the items through the `SageList` `items` property. -This assumes that the contents of the list are already formatted as desired. +`SageList` is implemented most simply by passing the desired contents for the items through the `SageList` `items` property. +This assumes that the contents of the list are already formatted as desired or you're outputting simple content values. ", use_sage_type: true) %> <%= sage_component SageList, { + items: sample_products.map { | item | { + content: "Item #{item[:name]}", + id: "example-default-item-#{item[:id]}", + more_actions: { items: dropdown_items(item[:id]) }, + } + }, +} %> + +

Sortable configuration

+<%= md(" +Sortable lists can be created with `SageList` by adding `sortable: true` and a `sortable_resource`. +You can also pass a `sortable_update_url` for sorting callbacks to items +that will be called after the user finishes sorting an item. +", use_sage_type: true) %> + +<%= sage_component SageList, { + sortable: true, sortable_resource: "sample_products", items: sample_products.map { | item | { - content: %( -

Item #{item[:name]}

-

Item #{item[:id]} specs

- ).html_safe, - id: item[:id], + content: "Item #{item[:name]}", + id: "example-sortable-item-#{item[:id]}", more_actions: { items: dropdown_items(item[:id]) }, - sortable: true, sortable_update_url: "#sortable-update-url?item=#{item[:id]}", } - } + }, +} %> + +

Fully draggable row

+<%= md(' +By default only the drag handle is active for dragging/sorting a row. +However, `drag_handle_type` can be set to `"row"` in order to allow the whole row to be draggable instead. +', use_sage_type: true) %> + +<%= sage_component SageList, { + sortable: true, + sortable_resource: "sample_products_2", + items: sample_products.map { | item | { + content: "Item #{item[:name]}", + id: "example-fully-draggable-item-#{item[:id]}", + more_actions: { items: dropdown_items(item[:id]) }, + sortable_update_url: "#sortable-update-url?item=#{item[:id]}", + }}, + drag_handle_type: "row", } %>

Compositional approach

<%= md(" -If you need more native content formatting you can instead opt to render items using a nested loop and the SageListItem component. +If you need more native content formatting you can instead opt to render items using a nested loop and the `SageListItem` component. +Note that this example also implements the [Reveal utility class](#{pages_helpers_path(:reveal)}). ", use_sage_type: true) %> -<%= sage_component SageList, { sortable_resource: "sample_products_2" } do %> +<%= sage_component SageList, { + sortable: true, + sortable_resource: "sample_products_3", + drag_handle_type: "row", +} do %> <% sample_products.each do | item | %> <%= sage_component SageListItem, { - id: item[:id], + id: "example-default-item-#{item[:id]}", more_actions: { items: dropdown_items(item[:id]) }, - sortable: true, sortable_update_url: "#sortable-update-url?item={item[:id]}", css_classes: SageClassnames::REVEAL_CONTAINER, } do %> <%= sage_component SageCardRow, { grid_template: "ete" } do %> - + <%= sage_component SageCardBlock, {} do %>

<%= item[:name] %>

Item <%= item[:id] %> specs

diff --git a/docs/app/views/examples/components/list/_props.html.erb b/docs/app/views/examples/components/list/_props.html.erb index 4a6eb704cb..b82b551784 100644 --- a/docs/app/views/examples/components/list/_props.html.erb +++ b/docs/app/views/examples/components/list/_props.html.erb @@ -9,18 +9,21 @@ <%= md('See schema for `SageListItem` below.') %> <%= md('`[]`') %> + + <%= md('`drag_handle_type`') %> + <%= md(' + By default only the drag handle is active for dragging/sorting a row. + However, `drag_handle_type` can be set to `"row"` in order to allow the whole row to be draggable instead. + ') %> + <%= md('`"default"` | `"row"` ') %> + <%= md('`"default"`') %> + <%= md('`sortable_resource`') %> <%= md('Provide the resource name for a sortable list. Required only when `sortable` is activated for any child items') %> <%= md('String') %> <%= md('`nil`') %> - - <%= md('`hide_first_border`') %> - <%= md('Removes top border from first list item within a SageList') %> - <%= md('Boolean') %> - <%= md('`false`') %> - <%= md("**SageListItem**") %> diff --git a/docs/lib/sage_rails/app/sage_tokens/sage_schemas.rb b/docs/lib/sage_rails/app/sage_tokens/sage_schemas.rb index a8e1dca051..1a9516fb5e 100644 --- a/docs/lib/sage_rails/app/sage_tokens/sage_schemas.rb +++ b/docs/lib/sage_rails/app/sage_tokens/sage_schemas.rb @@ -143,15 +143,15 @@ module SageSchemas LIST_ITEM = { id: [:optional, Integer, String], more_actions: [:optional, NilClass, SageSchemas::DROPDOWN], - sortable: [:optional, NilClass, TrueClass], sortable_update_url: [:optional, NilClass, String], } LIST = { + drag_handle_type: [:optional, NilClass, Set.new(["default", "row"])], items: [:optional, [[SageSchemas::LIST_ITEM]]], + sortable: [:optional, NilClass, TrueClass], sortable_resource: [:optional, NilClass, String], tag: [:optional, NilClass, Set.new(["ul", "ol"])], - hide_first_border: [:optional, TrueClass, String], } PANEL_FIGURE = { diff --git a/docs/lib/sage_rails/app/views/sage_components/_sage_list.html.erb b/docs/lib/sage_rails/app/views/sage_components/_sage_list.html.erb index 7c7a2d9b66..92ed7ad808 100644 --- a/docs/lib/sage_rails/app/views/sage_components/_sage_list.html.erb +++ b/docs/lib/sage_rails/app/views/sage_components/_sage_list.html.erb @@ -1,11 +1,14 @@ <% -tag = component.tag.present? ? component.tag : "ul" +tag = component.sortable ? "ol" : "ul" +tag = component.tag || tag +drag_handle_type = component.drag_handle_type.present? ? component.drag_handle_type : "default" %> <<%= tag %> class=" sage-list <%= component.generated_css_classes %> - <%= "sage-list--hide-first-border" if component.hide_first_border %> + <%= "sage-list--sortable" if component.sortable %> + <%= "sage-list--draggable-by-row" if drag_handle_type == "row" %> " <%= "data-js-list-sortable=#{component.sortable_resource}" if component.sortable_resource.present? %> <%= component.generated_html_attributes.html_safe %> @@ -15,6 +18,5 @@ tag = component.tag.present? ? component.tag : "ul" <%= sage_component SageListItem, item_configs %> <% end %> <% end %> - <%= component.content %> > diff --git a/docs/lib/sage_rails/app/views/sage_components/_sage_list_item.html.erb b/docs/lib/sage_rails/app/views/sage_components/_sage_list_item.html.erb index de8ceac248..6159106435 100644 --- a/docs/lib/sage_rails/app/views/sage_components/_sage_list_item.html.erb +++ b/docs/lib/sage_rails/app/views/sage_components/_sage_list_item.html.erb @@ -10,18 +10,15 @@ end
  • <%= component.generated_css_classes %> " <%= "data-js-list-sortable-update-url=#{component.sortable_update_url}" if component.sortable_update_url.present? %> <%= "id=#{component.id}" if component.id %> <%= component.generated_html_attributes.html_safe %> > - <% if component.sortable %> -
    - <%= sage_component SageIcon, { icon: "handle" } %> -
    - <% end %> +
    + <%= sage_component SageIcon, { icon: "handle-2-vertical" } %> +
    <%= component.content %> diff --git a/packages/sage-assets/lib/stylesheets/components/_list.scss b/packages/sage-assets/lib/stylesheets/components/_list.scss index a18ea70d21..335225cfc6 100644 --- a/packages/sage-assets/lib/stylesheets/components/_list.scss +++ b/packages/sage-assets/lib/stylesheets/components/_list.scss @@ -13,45 +13,73 @@ .sage-list__item { display: flex; align-items: center; - gap: sage-spacing(md); - padding-top: sage-spacing(sm); - padding-bottom: sage-spacing(sm); + gap: sage-spacing(sm); + padding: sage-spacing(xs) rem(12px); list-style: none; - border-top: sage-border(default); + border-radius: sage-border(radius-medium); - &:first-child { - .sage-list--hide-first-border & { - border-top: 0; - } + &:hover { + background-color: sage-color(grey, 200); + } + + .sage-list--draggable-by-row & { + cursor: grab; } } -.sage-list__item--sortable { - cursor: grab; +.sage-list__item--sortable-active { + background-color: sage-color(white); - &.sage-list__item--sortable-active:active, - &:active { + .sage-list--draggable-by-row & { cursor: grabbing; } } +.sage-list__item--sortable-drag { + background-color: sage-color(white); + box-shadow: sage-shadow(lg); + // NOTE: !important is added and lint-ignored here so that it can override + // inline opacity that the SortableJS utility we're using adds during sorting. + opacity: 1 !important; /* stylelint-disable-line declaration-no-important */ +} + .sage-list__item--sortable-ghost { - opacity: 0.5; + background-color: sage-color(grey, 200); } .sage-list__item-content { flex: 1; + + .sage-list__item--sortable-ghost & { + opacity: 0; + } } -.sage-list__item-more-actions, -.sage-list__item-sortable-handle { +.sage-list__item-more-actions { width: auto; + + .sage-list__item--sortable-ghost & { + opacity: 0; + } } .sage-list__item-sortable-handle { - color: sage-color(charcoal, 100); + display: none; + + .sage-list--sortable & { + display: initial; + width: auto; + } + + &:hover { + cursor: grab; + } + + .sage-list__item--sortable-active & { + cursor: grabbing; + } - .sage-list__item:hover & { - color: sage-color(charcoal, 500); + .sage-list__item--sortable-ghost & { + opacity: 0; } } diff --git a/packages/sage-assets/lib/stylesheets/components/_lists.scss b/packages/sage-assets/lib/stylesheets/components/_lists.scss index 0e5aeb5685..bf1e7c4fbb 100644 --- a/packages/sage-assets/lib/stylesheets/components/_lists.scss +++ b/packages/sage-assets/lib/stylesheets/components/_lists.scss @@ -8,8 +8,9 @@ .sage-list { padding-left: 0; list-style: none; - - &:not(.sage-list--inline-compact):not(.sage-list--inline-fit-compact) { + + // TODO: These styles interfered with the new List component and should be deprecated. + &:not(.sage-list--inline-compact):not(.sage-list--inline-fit-compact):not(.sage-list__item) { margin-right: -1 * sage-spacing(sm); margin-left: -1 * sage-spacing(sm); diff --git a/packages/sage-react/lib/List/List.jsx b/packages/sage-react/lib/List/List.jsx index 9dba7448f8..0efb6cf2d9 100644 --- a/packages/sage-react/lib/List/List.jsx +++ b/packages/sage-react/lib/List/List.jsx @@ -3,25 +3,31 @@ import classnames from 'classnames'; import PropTypes from 'prop-types'; import { ReactSortable } from 'react-sortablejs'; import { ListItem } from './ListItem'; -import { OptionsDropdown } from '../Dropdown'; +import { LIST_DRAG_HANDLE_TYPES } from './configs'; export const List = ({ children, className, - hideFirstBorder, + dragHandleType, items, itemRenderer, - sortableConfigs, + onEnd, + onStart, + setList, + sortable, tag, }) => { const classNames = classnames( 'sage-list', className, { - 'sage-list--hide-first-border': hideFirstBorder, + 'sage-list--sortable': sortable, + 'sage-list--draggable-by-row': dragHandleType === LIST_DRAG_HANDLE_TYPES.ROW, } ); + const draggingClassname = 'sage-list--sortable-dragging'; + const renderItems = () => { if (children) { return children; @@ -30,7 +36,7 @@ export const List = ({ return ( <> {items.map(({ id, ...rest }) => ( - + {itemRenderer && itemRenderer({ id, ...rest })} ))} @@ -38,17 +44,37 @@ export const List = ({ ); }; - const Tag = tag; + let Tag = sortable ? 'ol' : 'ul'; + Tag = tag || Tag; - return sortableConfigs ? ( + return sortable ? ( { + evt.srcElement.classList.remove(draggingClassname); + if (onEnd) { + onEnd(evt); + } + }} + onStart={(evt) => { + evt.srcElement.classList.add(draggingClassname); + if (onStart) { + onStart(evt); + } + }} + setList={setList} + tag={Tag} > {renderItems()} @@ -60,42 +86,30 @@ export const List = ({ }; List.Item = ListItem; +List.DRAG_HANDLE_TYPES = LIST_DRAG_HANDLE_TYPES; List.defaultProps = { children: null, className: null, - hideFirstBorder: false, items: [], - itemRenderer: null, - sortableConfigs: null, - tag: 'ul', + itemRenderer: () => {}, + dragHandleType: List.DRAG_HANDLE_TYPES.DEFAULT, + onEnd: () => {}, + onStart: () => {}, + setList: () => {}, + sortable: false, + tag: null, }; List.propTypes = { children: PropTypes.node, className: PropTypes.string, - hideFirstBorder: PropTypes.bool, - items: PropTypes.arrayOf( - PropTypes.shape({ - children: PropTypes.node, - chosen: PropTypes.bool, // From react-sortablejs - className: PropTypes.string, - filtered: PropTypes.bool, // From react-sortablejs - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - moreActions: PropTypes.shape({ ...OptionsDropdown.propTypes }), - sortable: PropTypes.bool, - selected: PropTypes.bool, // From react-sortablejs - }) - ), + dragHandleType: PropTypes.oneOf(Object.values(List.DRAG_HANDLE_TYPES)), + items: PropTypes.arrayOf(PropTypes.shape({ ...ListItem.propTypes })), itemRenderer: PropTypes.func, - sortableConfigs: PropTypes.shape({ - onEnd: PropTypes.func, - setList: PropTypes.func, // Same as useState[1] - tag: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.elementType, - ]), - // NOTE: See https://github.com/SortableJS/react-sortablejs for full list of additional options - }), + onEnd: PropTypes.func, + onStart: PropTypes.func, + setList: PropTypes.func, // Same as useState[1] + sortable: PropTypes.bool, tag: PropTypes.oneOf(['ul', 'ol']), }; diff --git a/packages/sage-react/lib/List/List.story.jsx b/packages/sage-react/lib/List/List.story.jsx index 015318a6c3..6835b32ff4 100644 --- a/packages/sage-react/lib/List/List.story.jsx +++ b/packages/sage-react/lib/List/List.story.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { selectArgs } from '../story-support/helpers'; import { List } from './List'; import { sampleItems, @@ -8,7 +9,11 @@ import { export default { title: 'Sage/List', component: List, - argTypes: {}, + argTypes: { + ...selectArgs({ + dragHandleType: List.DRAG_HANDLE_TYPES, + }), + }, args: { items: [], }, @@ -28,7 +33,21 @@ export const SortableList = () => { + ); +}; + +export const FullyDraggableSortableList = () => { + const [items, setItems] = React.useState(sampleItems); + return ( + ); }; diff --git a/packages/sage-react/lib/List/ListItem.jsx b/packages/sage-react/lib/List/ListItem.jsx index d01bae2e13..5335707b50 100644 --- a/packages/sage-react/lib/List/ListItem.jsx +++ b/packages/sage-react/lib/List/ListItem.jsx @@ -23,7 +23,7 @@ export const ListItem = ({
  • {sortable && (
    - +
    )}
    diff --git a/packages/sage-react/lib/List/configs.js b/packages/sage-react/lib/List/configs.js new file mode 100644 index 0000000000..f5d59bf5d3 --- /dev/null +++ b/packages/sage-react/lib/List/configs.js @@ -0,0 +1,4 @@ +export const LIST_DRAG_HANDLE_TYPES = { + DEFAULT: 'default', + ROW: 'row', +}; diff --git a/packages/sage-system/lib/list.js b/packages/sage-system/lib/list.js index a0240b7118..2c25f50d77 100644 --- a/packages/sage-system/lib/list.js +++ b/packages/sage-system/lib/list.js @@ -1,3 +1,5 @@ +// NOTE: Uses SortableJS +// https://github.com/SortableJS/Sortable import Sortable from 'sortablejs/modular/sortable.core.esm.js'; Sage.sortableList = (function() { @@ -5,25 +7,32 @@ Sage.sortableList = (function() { // Variables // ================================================== + const DRAGGING_CLASSNAME = 'sage-list--sortable-dragging'; const SELECTOR_CONTAINER = 'data-js-list-sortable'; + const SELECTOR_DRAGGABLE_BY_ROW = 'sage-list--draggable-by-row'; const SELECTOR_ITEM_UPDATE_URL = 'data-js-list-sortable-update-url'; - const SETTINGS = { - ghostClass: 'sage-list__item--sortable-ghost', - chosenClass: 'sage-list__item--sortable-active', - }; // ================================================== // Functions // ================================================== - function init(el) { + const init = (el) => { let resourceName = el.getAttribute(SELECTOR_CONTAINER); if (!resourceName) return console.error(`Sage Sortable requires a resource name \n\n EXAMPLE: \n [${SELECTOR_CONTAINER}="resourceName"]`); Sortable.create(el, { - ...SETTINGS, + chosenClass: 'sage-list__item--sortable-active', + dragClass: 'sage-list__item--sortable-drag', + forceFallback: true, // NOTE: This is added because Safari 13+ has a draggable api bug https://github.com/SortableJS/Sortable/issues/1571 + ghostClass: 'sage-list__item--sortable-ghost', + handle: el.classList.contains(SELECTOR_DRAGGABLE_BY_ROW) ? false : '.sage-list__item-sortable-handle', + onStart: function(evt) { + evt.srcElement.classList.add(DRAGGING_CLASSNAME); + }, onEnd: function (evt) { + evt.srcElement.classList.remove(DRAGGING_CLASSNAME); + let updateUrl = evt.item.getAttribute(SELECTOR_ITEM_UPDATE_URL) // Check if the sorted Item: @@ -35,19 +44,18 @@ Sage.sortableList = (function() { params.append('_method', 'PUT'); params.append(`${resourceName}[sort_position]`, evt.newIndex); - Sage.util.ajaxRequestWithJsInjection('POST', updateUrl, params) - } + Sage.util.ajaxRequestWithJsInjection('POST', updateUrl, params); + }, }); - } + }; - function unbind(el) { + const unbind = (el) => { let sortableInstance = Sortable.get(el); sortableInstance.destroy(); - } + }; return { - init: init, - unbind: unbind - } - + init, + unbind, + }; })();