GithubHelp home page GithubHelp logo

schne324 / dragon-drop Goto Github PK

View Code? Open in Web Editor NEW
402.0 9.0 31.0 1.31 MB

Accessible drag and drop list reorder module

Home Page: https://schne324.github.io/dragon-drop/demo/

License: MIT License

JavaScript 100.00%
drag-and-drop a11y dragon-drop accessible

dragon-drop's Introduction

Dragon Drop

CircleCI

Keyboard/assistive technology accessible drag-and-drop reorder list.

Dragon Drop

Demo

http://schne324.github.io/dragon-drop/demo/

Enter the dragon...

For an in-depth look at dragon drop see the smashing magazine article on dragon drop

Installation

npm

$ npm install drag-on-drop

bower

$ bower install drag-on-drop

Usage

Browserify/Webpack

import DragonDrop from 'drag-on-drop';

const dragon = new DragonDrop(container, options);

In the browser

const DragonDrop = window.DragonDrop;
const dragon = new DragonDrop(container, options);

React

Although a DragonDrop react component doesn't exist (yet), it can be used with react:

class App extends Component {
  componentDidMount() {
    this.setState({
      dragonDrop: new DragonDrop(this.dragon)
    });
  }

  componentDidUpdate() {
    const { dragonDrop } = this.state;
    // this public method allows dragon drop to
    // reassess the updated items and handles
    dragonDrop.initElements(this.dragon);
  }

  render() {
    return (
      <ul className='dragon' ref={el => this.dragon = el}>
        <li>
          <button type='button' aria-label='Reorder' />
          <span>Item 1</span>
        </li>
        <li>
          <button type='button' aria-label='Reorder' />
          <span>Item 2</span>
        </li>
      </ul>
    );
  }
}

Full example

NOTE usage with react is not exactly ideal because DragonDrop uses normal DOM events not picked up by react (react doesn't know about the reordering).

CDN (unpkg)

https://unpkg.com/drag-on-drop

API

new DragonDrop(container, [options])

container HTMLElement|Array (required)

Either a single container element or an array of container elements.

options Object (optional)

item String

The selector for the drag items (qualified within container). Defaults to

'li'
handle String

The selector for the keyboard handle (qualified within the container and the selector provided for item). If set to false, the entire item will be used as the handle. Defaults to

'button'
activeClass String

The class to be added to the item being dragged. Defaults to

'dragon-active'
inactiveClass String

The class to be added to all of the other items when an item is being dragged. Defaults

'dragon-inactive'
nested Boolean

Set to true if nested lists are being used (click and keydown events will not bubble up (e.stopPropagation() will be applied)). For nested lists, you MUST pass DragonDrop an array of containers as the 1st parameter (see example below).

NOTE: there is a 99% chance that you'll need to use :scope selectors to target only a given list's items (because dragon drop would otherwise include the sub list's items for example). Using :scope selectors will allow you to target direct descendant children (example: :scope > li).

const lists = Array.from(document.querySelectorAll('.dragon-list'));
const dragons = new DragonDrop(lists, {
  nested: true,
  handle: false,
  item: ':scope > li' // IMPORTANT! a selector that targets only a single list's items
});
const [ topLevel, sublist1, sublist2 ] = dragons;

topLevel.on('grabbed', () => console.log('top-most container item grabbed'));
sublist1.on('grabbed', () => console.log('sublist 1 item grabbed'));
sublist2.on('grabbed', () => console.log('sublist 1 item grabbed'));
dragulaOptions Object

An options object passed through to dragula.

NOTE: dragulaOptions.moves will be ignored given a DragonDrop instance with nested: false and a truthy handle

NOTE: dragulaOptions.moves AND dragulaOptions.accepts will be ignored given a DragonDrop instance with nested: true

announcement Object

The live region announcement configuration object containing the following properties:

grabbed Function

The function called when an item is picked up. The currently grabbed element along with an array of all items are passed as arguments respectively. The function should return a string of text to be announced in the live region. Defaults to

el => `Item ${el.innerText} grabbed`
dropped Function

The function called when an item is dropped. The newly dropped item along with an array of all items are passed as arguments respectively. The function should return a string of text to be announced in the live region. Defaults to

el => `Item ${el.innerText} dropped`
reorder Function

The function called when the list has been reordered. The newly dropped item along with an array of items are passed as arguments respectively. The function should return a string of text to be announced in the live region. Defaults to

(el, items) => {
  const pos = items.indexOf(el) + 1;
  const text = el.innerText;
  return `The list has been reordered, ${text} is now item ${pos} of ${items.length}`;
}
cancel Function

The function called when the reorder is cancelled (via ESC). No arguments passed in. Defaults to

() => 'Reordering cancelled'
liveRegion Object

Attributes that can be overridden in on the live region:

ariaLive string

Optional ariaLive attribute to be passed to the live region. Valid values are "off", "polite", or "assertive". Default is "assertive".

ariaRelevant string

Optional ariaRelevant attribute to be passed to the live region. Valid values are "additions", "removals", "text", and "all". Default is "additions".

ariaAtomic boolean

Optional ariaAtomic attribute to be passed to the live region. Default is "true".

Properties

const dragonDrop = new DragonDrop(container);

dragonDrop.items Array

An array of each of the sortable item element references.

dragonDrop.handles Array

An array of each of the handle item element references. If instance doesn't have handles, this will be identical to dragonDrop.items.

dragonDrop.dragula

A direct handle on the dragula instance created by dragonDrop

Example with options

const list = document.getElementById('dragon-list');
const dragonDrop = new DragonDrop(list, {
  item: 'li',
  handle: '.handle',
  announcement: {
    grabbed: el => `The dragon has grabbed ${el.innerText}`,
    dropped: el => `The dragon has dropped ${el.innerText}`,
    reorder: (el, items) => {
      const pos = items.indexOf(el) + 1;
      const text = el.innerText;
      return `The dragon's list has been reordered, ${text} is now item ${pos} of ${items.length}`;
    },
    cancel: 'The dragon cancelled the reorder'
  }
});

Events

Dragon drop emit events when important stuff happens.

dragonDrop.on('grabbed', callback)

Fires when an item is grabbed (with keyboard or mouse). The callback is passed the container along with the grabbed item.

dragonDrop.on('dropped', callback)

Fires when an item is dropped (with keyboard or mouse). The callback is passed the container and the grabbed item.

dragonDrop.on('reorder', callback)

Fires when a list is reordered. The callback is passed the container along with the item.

dragonDrop.on('cancel', callback)

Fires when a user cancels reordering with the escape key. The callback is passed the keydown event that triggered the cancel.

Example use of events

dragonDrop
  .on('grabbed', (container, item) => console.log(`Item ${item.innerText} grabbed`))
  .on('dropped', (container, item) => console.log(`Item ${item.innerText} dropped`))
  .on('reorder', (container, item) => console.log(`Reorder: ${item.innerText} has moved`))
  .on('cancel', (e) => {
    console.log('Reordering cancelled');
    e.preventDefault();
  });

NOTE for mouse drag/drop event hooks the dragula property is exposed for dragula's events

dragonDrop.dragula.on('drop', ...);

Methods

dragonDrop.initElements(container)

Reinitialises the list, so that newly added items can be dragged. You can do this automatically with a MutationObserver:

const observer = new MutationObserver(() => dragonDrop.initElements(container));
observer.observe(container, {childList: true});

Debugging

Set the following localStorage option to debug dragonDrop

localStorage.debug = 'drag-on-drop:*';

Notes on accessibility

There are certain things that are left up to the discretion of the implementer. This is to keep DragonDrop less opinionated on some of the gray areas of a11y. The demos show a few different approaches on associating help text with DragonDrop:

  1. Recommended aria-describedby on each control (item, or if applicable, handle). This is the safest/test approach because it guarantees that AT users will receive the instructions. Demo of this approach
  2. Recommended aria-labelledby on the container/list element. With this approach, where supported, will announce the instructions whenever the AT users enters the list (which is less verbose than the above). Demo of this approach
  3. Not recommendedaria-describedby on the container/list element. This approach, where supported, will only announce the instructions if the screen reader user traverses to the actual list element. Demo of this approach

For more information regarding accessibility you can read an accessibility review of dragon drop initiated by Drupal.

Thanks!

A special thanks to Aaron Pearlman for the logo.

Another special thanks to contributors/maintainers of dragula which is used for all of the mouse behavior/interaction for dragon drop!

dragon-drop's People

Contributors

dependabot[bot] avatar okry avatar schne324 avatar tdhooper avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dragon-drop's Issues

Question: Unit Testing with DragonDrop

Hello again,

I was wondering if you have any recommendations for unit testing the dragon-drop functionality in an Angular app. I've set up my code according to the documentation:

import DragonDrop from 'drag-on-drop';

// Other component stuff goes here

this.keyboardDragon = new DragonDrop(this.dragon.nativeElement, {
   item: 'mapping-item-source-dest-position',
   handle: false,
   activeClass: 'gu-mirror-keyboard'
});

this.keyboardDragon.on('dropped', (_, item) => {
  // Do some stuff
});

But when I run my jasmine tests, I get the following error:
TypeError: drag_on_drop_1.default is not a constructor

Any ideas?

New items cannot be dragged

After initialisation, new items added to the container can be moved with the mouse, but not the keyboard.

For an example, go to https://schne324.github.io/dragon-drop/demo/ and add a new item to the first demo by running this in the console:

$('#demo-1').appendChild($('#demo-1').lastElementChild.cloneNode(true))

Tabbing onto the new handle and pressing return does nothing, however dragging it with the mouse works.

Demo on mobile not working as expected

Hi. Thanks for working through this problem so the rest of us don't have to.

I'm interested in using the library but testing it on my iPhone 8 Plus in Chrome on iOS 13.1.2 (latest OS as of this writing), I noticed it's not working as neatly as the desktop version: it seems that pressing and holding on the draggable DIVs in the list does not actually take until the browser goes into its text-select mode (meaning the mode you enter to select some text in a paragraph, for example). You can see the iOS blue text-select highlight box while you drag in some cases, (which could be dealt with with user-select: none; as a style possibly), but most importantly, there is a delay in between touching the div and it actually allowing you to drag, so if you touch and drag immediately, you scroll the page.

Is this intended as a mobile solution as well or am I barking up the wrong tree here?

Thanks very much.

Feature Request: Support for ng2-dragula

I'm working on an project that uses Angular 6 and would like to use this library for the keyboard interactions, and the Angular 2 bridge for dragula for the mouse interactions (ie. ng2-dragula).

In my project, I have a list of items that are sorted by a position property. When you re-order the items on the list, via dragging an item with the mouse or keyboard, the position property for all the items in the array is updated.

ng2-dragula has a new event available in the service called dropModel; an observable that sends over a few properties, most notably the targetModel, so I can easily update the positions of each item in the array. This takes care of the above paragraph for mouse interactions.

However, I'm struggling a bit when using this library to figure out how to do this for the keyboard interactions. I have some ideas for features that could make this easier:

  1. Include the array of items in the .on('dropped') callback function, so that there's some way to determine new positions with the dropped item.
  2. Add a property to the options object that lets you pass in the dragula/drake instance created with ng2-dragula. As it stands now, I think I'm creating two - one with your lib, and one with ng2-dragula. Not sure this would solve my problem with the keyboard events though.
  3. Other?

Please let me know if there is something I'm missing or any other ideas you may have.

Callback functions are not called, even on demo page; reorder announcement never fires either

Hi,

In both my own code and on the demo page at https://schne324.github.io/dragon-drop/demo/ none of the callback functions are executed. Given this basic example:

dragonDrop
            .on('grabbed', (container, item) => console.log(`Item ${item.innerText} grabbed`))
            .on('dropped', (container, item) => console.log(`Item ${item.innerText} dropped`))
            .on('reorder', (container, item) => console.log(`Reorder: ${item.innerText} has moved`))
            .on('cancel', () => console.log('Reordering cancelled'));

None of these log events appear in the console. And this is true for me even on the demo page at https://schne324.github.io/dragon-drop/demo/ .

Further, on the demo page the reorder announcement is never displayed, only grabbed and dropped. Browser breakpoints do fire for grabbed and dropped; no breakpoint fires for reorder (in Chrome, Firefox, Edge). If I add localStorage.debug = 'drag-on-drop:*'; I can see console logs for grabbed and dropped, but again, not for reorder.

Normally I'd be happy to admit that my code is wrong, but I can't see it working on the demo page either :(.

Thanks,

MirrorContainer property missing.

Hello,

I have this issue where the mirrored (moving) div is appended in the body. In dragula, we have the option of passing a scope with the attribute mirrorContainer: yourcontainer.

dragula([this.reorderableList], { mirrorContainer: this.reorderableList });

Is it possible to have this option accessible from the Drag on Drop interface ? I can't manage to make it work without it...

Thank you !

'Browse mode' not correctly supported.

We had some accessibility problems with this, reproducible on the demo page: I tried the "rank the bands" thing with NVDA running, but because it uses an OL to group the elements, the screen reader is in browse mode, which means it 'eats' the arrow keystrokes. (OL is not an operable element type).

Lots of accessibility jargon there, you might need guidance, so there is some background on screen reader interaction modes here.

So it means that if you have NVDA running, you can grab and drop the items, but not actually move them. Other screen readers and assistive tech (notably JAWS) will have the same problem.

Possible Solution: Add role=application to the OL, and 'it just works' because it puts the assistive tech into 'forms mode' (also known as 'application mode'). Other operable roles might be more appropriate in a given case. Screen reader announcements are a bit chatty but communicate the right info otherwise.

I am not sure if this is the perfect solution, because it depends on the semantic context where this gadget will end up, but it's relatively easy to do and certainly makes dragon-drop work with NVDA, probably JAWS too. Accessible drag-and-drop is quite tricky, so that's a win, I should think!

Dragula - Cannot read property 'on' of undefined

The docs say to use dragonDrop.dragula.on('drop', ...); for mouse events, but no matter what I do I can't seem to actually access dragula. Using the 'nested lists' example from the demo, I've tried:

  • DragonDrop.dragula.on('drop', ...)
  • dragons.dragula.on('drop', ...)
  • dragon.dragula.on('drop', ...) (within dragons.forEach)

Each of those seems to throw Uncaught TypeError: Cannot read property 'on' of undefined. Any thoughts?

Feature Request: support for dragging between multiple containers

I have created a drag and drop project whose basis is to allow items from container 1 to be draggable to container 2 and vice versa, as well as allowing items to be reordered within their containers. For the first iteration of my project I have achieved this using dragula.

However, I need to make this accessible and was looking at using dragon-drop, however I don't think it is currently possible to implement dragon-drop in this way, is that correct?

Can't delegate click on child element

I have the need to add delete buttons on each sublist element, but I cannot seem to delegate a click event on my buttons because the event is always handled by DragonDrop. The list items and delete buttons are being added dynamically. Any suggestions?

Dropped event is fired on cancellation

I have a list of items that I need to update on the server when a reorder is performed. To do this I hook into the dropped event and perform an ajax call . However, it appears that the dropped event fires even when a user cancels the re-order with esc. Is this intended functionality?

Using keyboard to reorder dynamically added list entries

I have an issue with adding new list entries after DragonDrop was applied to a list.
In my case I have a list containing a few items and apply DragonDrop to this list. Everything works here as expected.

At some point I want to add a new list entry to this list dynamically. After adding this entry (by using the jQuery.append() function) the reordering using the mouse works fine but selecting the new entry is not possible and thus the reordering using the keyboard also does not work.

I would consider this as an issue or is there any way to "refresh" DragonDrop or completely "destroy" DragonDrop on this list and reapply it once the new list item is added?

I've prepared a little example here. There is a button below the list which adds a new list entry dynamically. After the new entry is added you can't reorder it by using the keyboard.

Feature request: Nested lists

My project uses drag-and-drop interfaces that are not currently accessible, and I'd love to use dragon drop to replace our current sorting jquery plugin. Many of our interfaces are for creating navigation lists, and so the ability to nest the sortable items is key.

XSS vulnerability in default announcements

Hi,

There's an XSS vulnerability while rendering the LiveRegion when the list is generated with user input. I also managed to recreate it in the demo.

Steps to recreate:

  1. Go to the live demo on https://schne324.github.io/dragon-drop/demo/
  2. Replace on of the labels, for instance Ween with "><img src=x onerror="alert('Evil script');" />. (Make sure that the text in the dom is encoded, so "&gt;&lt;img src=x onerror="alert('Evil script');" /&gt;.)
  3. Drag & drop the item with malicious content.
  4. You'll be prompted with the Js alert.

The problem lies with the use of HTMLElement.innerText for the default announcements.

  announcement: {
    grabbed: function grabbed(el) {
      return 'Item ' + el.innerText + ' grabbed';
    },
    dropped: function dropped(el) {
      return 'Item ' + el.innerText + ' dropped';
    },
    reorder: function reorder(el, items) {
      var pos = items.indexOf(el) + 1;
      var text = el.innerText;
      return 'The list has been reordered, ' + text + ' is now item ' + pos + ' of ' + items.length;
    },
    cancel: 'Reordering cancelled'
  }

Some documentation on XSS prevention can be found here https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html.

Bug: Clicking on handle creates focused state

Description
When I click on the drag handle, it puts that element in a state of aria-pressed="true" data-drag-on="true". I only want those things to happen when the element is truly being dragged.

Steps to Repro:

  1. In the "Rank the bands" example on the demo page, click on the "Leftover salmon" handle (handle/item is in the state described above).
  2. Click anywhere else on the page or tab to put focus on the first handle.

Expected Result
State described above is only applied in a true "grab" (when I click+drag or when I hit the space/enter key).

Actual Result
State is applied on click of the item and cannot be removed, unless the user selects a different item from the list with the mouse or keyboard.

Additional Info
I had an idea for fixing this:

  1. Create a custom event in the onKeyDown method - const event = new Event('grab');
  2. Then dispatch that event where it's calling target.click() --> target.dispatchEvent(event)
  3. In initClick() set each handle to listen for the new event handle.addEventListener('grab', ...)
  4. Add a document.addEventListener('click', ...) to remove any of the "grabbed" state attributes, if the user clicks anywhere else in the document.

#4 is maybe not the best solution, but an idea. Let me know what you think of the above.

Nested Handles

We want to use handles and use them with nested lists. It seems when you are nested, it only supports that you move the whole li and it does not use handles. Is there a way to have handle buttons on sub items or is that only supported with 1 level?

Bug: Demo does not work in IE

Description:
I was having an issue opening my project in IE, so I wanted to make sure the demo was working in IE 11. Doesn't look like I'm able to do many of the interactions, with the keyboard or the mouse.

Steps to Repro:

  1. Open the demo in an IE11 browser window: https://schne324.github.io/dragon-drop/demo/
  2. Tab to the first example to put the handle in focus
  3. Press the Space bar and either of the arrow keys to move the item, OR
  4. Click and drag with the mouse to move the item

Expected Result:
Item moves up/down, as seen in Chrome

Actual Result:
Handle is in focus, but not selected. Arrow keys scroll browser window.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.