Thursday, March 5, 2020

Faux Drag and Drop using Hidden Sliders

One of the key features missing from a UI perspective from PowerApps currently is the ability to drag/drop items on the screen.  It definitely limits some of your options in building interfaces and controls that work in a way users might expect.

However, there are notes elsewhere on how to mimic this behavior in some limited fashion using sliders.  This walks you through a common use case of having a Gallery where you can move items into various stages similar to a Kanban board in a manner that will seem intuitive to users.



Overview


What we'll be creating is a hidden Slider control, putting that on top of the other items, then changing the X value of the "moving object" underneath that based upon the Slider value (or alternatively the Y value for a vertical Slider).  Now, depending upon how "smooth" you want the "drag & drop" to appear, you could set the Slider's value quite high and let the object move along smoothly across the screen.  However, if you want the objects essentially to snap-to a location, then setting a much lower series of values makes sense.

In the example below, I'm going to do a "snap-to" location because it both feels natural to the user but also doesn't fully try to mimic the traditional drag & drop style of control so they aren't tempted to try dragging it in any direction other than what we want them to do.

Just know that you could really make your control slide along the screen smoothly if you'd like.  You would just need to handle if/when people would drop the item into areas that aren't intended for things to be dropped and to reset the Slider back to where it originated.

Basic Data


First things first, you're going to need a Collection to work with and populate your Gallery.  In this scenario, I'm going to pretend we're building a list of "People" who are moving through "Phases" of our process.  So I'll modify my App.OnStart event to create an appropriate Collection named peopleList.

ClearCollect(
    peopleList,
    {
        Name: "Person 1",
        Phase: 0
    },
    {
        Name: "Person 2",
        Phase: 0
    },
    {
        Name: "Person 3",
        Phase: 0
    }
)

From a UI perspective, I'm looking for something that quickly graphically shows us which phase a person is in currently.


To do this, I dropped in 4 labels, changed their colors to be gradients of Blue w/ a slightly lower opacity.  For example:

phase0Bckgrnd.Fill = RGBA(50,50,250,.6)
phase1Bckgrnd.Fill = RGBA(50,50,250,.4)
phase2Bckgrnd.Fill = RGBA(50,50,250,.2)
phase3Bckgrnd.Fill = RGBA(50,50,250,.05)

I dropped these into the back of my page so that they're all behind any control I'll be working with.


I also linked the Height/Width of each to the first item.  Then set the X location for the right 3 so that they reference the Background control to the left to figure out their position (e.g. phase1Bckgrnd.X = phase0Bckgrnd.X + phas0Bckgrnd.Width ).  So now, I can move the far left background to wherever I need it and the rest will follow.

Linking up the Data source

Now, we're going to drop a Gallery on top of this that will hold our People and some controls to show where they are within our process.

I am working with a blank Vertical Gallery and manually adding in my Datasource.  So I first connect my new Gallery to my Data source:
Now I add in some controls for my Gallery.  In my case, I'll use 3:
  • Label: for the name of the person
  • Text Input: that will create the moving object that the user will "think" they're sliding around to different Phases
  • Slider: that will actually control things and we'll eventually hide from the user
NOTE: I'm using a Text Input as my 2nd item here as I will allow my users to input some notes about the current state of the Phase directly via this screen if they want.  Plus I was being lazy and didn't want to explain which control is which over and over if I used 2 Labels.  :D

As a last step, I changed the color of my Text Input to have a Fill of Yellow, set the DisplayMode (for now) to View, and erased the Text value so it is just am empty Yellow rounded-corner rectangle.  This is the object that the user will see move left/right on the screen and it will appear that it is being dragged/dropped.

Linking up the Fields

Select your Gallery on the Left-hand side of the screen and then the Fields - Edit to the right side of the screen:
Now link up each of your fields to the appropriate column.  I'm using:
  • Label: peopleName => Name
  • Text Input: phaseIndicator => Phase
  • Slider: phaseSlider => Phase

Now your fields are linked to the data and we're ready to really work on the UI.

Set the Order and Location

First thing we need is to get thing in order per Back-to-Front.  Our Gallery is already in front of the Background Labels we used initially, but we need to make sure we have our items within the Gallery in order.  The Slider should be on top, Text Input behind that, and the Label doesn't really matter but I put mine in the back.

Now, the important thing to know (if you don't already) is that we are now working w/ some objects that are "outside" of the Gallery (the Backgrounds) and other objects that are "inside" the Gallery (Label, Text Input, and Slider).  This becomes important if you want to reference X or Y values of items /inside/in how you orient your controls.  Now we could just kind of punt and extend our Gallery to the edges of the screen and then all of the orientations would be identical.  However, I'm going to at least walk through keeping your Gallery slightly smaller than the screen.

First set up the height of the items within your Vertical Gallery (you'd be doing width for Horizontal) using the TemplateSize setting.  This controls how large of a space each individual item uses.  In my design, I'm setting mine to 120.  Do this by clicking the Gallery to the left side of the page, then (on the right) set the TemplateSize to 120.


Now, we need to get our Text Input to be almost as wide as our Background images and almost as tall as our TemplateSize.  The simplest method here is again to reference the other controls.

However, there's a little trick here where it comes to the Gallery's TemplatePadding value (default is 5).  If we use this value as a reference, then we can automatically pad our items and consistently pad individual controls similar to how we'll be padding the edges of our items within the Gallery.  This values is INSIDE of our TemplateSize rather than being outside.  So if we have a padding value of 5 and a size of 120, then our ACTUAL USABLE SIZE is 110 (120 - 5 - 5).

So do the following for the Text Input control:

  • Height = Parent.TemplateHeight-(Parent.TemplatePadding*2)
  • Width = phase0Bckgrnd.Width-(Parent.TemplatePadding*2)
    • phase0Bckgrnd is the name of my far-left Background control

What we're doing here is including a little padding around our object for purely aesthetic reasons and keeping it consistent.

However, what we haven't done yet is to move the control to it's starting X location (left->right) or Y location (top->bottom).  Again, for your Text Input control change:

  • Y = Parent.TemplatePadding
  • X = phase0Bckgrnd.X-Parent.X
    • NOTE: We'll be adding a bit to this equation very shortly - keep reading!

Whew!

Now our Text Input should be centered on the first Background item, but it doesn't move.  So how do we make it move around like we imagine it should?

This is where our Slider comes in.

Slider Setup and Linking To Text Input

The key to how we're going to emulate us sliding this yellow box left/right into each of the "Phases" will be using a hidden Slider.  If you haven't already added it, then add a Slider into your Gallery.

Select the Slider and edit the following values:

  • Default = ThisItem.Phase
    • This is the name of the field we're linking this to in our original Collection.
  • Min = 0
  • Max = 3
    • This is because we have 4 "Phases" or columns we're moving between (0-3)
  • Y = 0
  • Height = Parent.TemplateHeight
    • This makes it the full height of each individual Gallery item
  • X= phase0Bckgrnd.X-Parent.X
  • Width = phase0Bckgrnd.Width * 4
    • It is *4 because we have 4 Phases
  • RailThickness = phaseIndicator.Height
    • This allow us to select/slide the selector not only along the center of the line of travel but also the full height of the object that the user sees.
Go back and re-select the Text Input and now we're going to link it to the Slider's value.  For the Text Input change:
  • X = phase0Bckgrnd.X-Parent.X + phaseSlider.Value*phase0Bckgrnd.Width
    • This is moving our box so it "snaps" to the next column when we change the values on the Slider.
Now, we have a problem here.  This is where we need to make some choices per usability.  If you run this now, you'll notice that your slider and your columns don't completely line up.  This has to do w/ how the Slider component calculates where we are on the Slider.  If we are trying to use the "Snap To" method (small values for Slider - snaps to location instantly), then things just won't line up completely correctly.  We can approximate what a user might expect by updating the Slider values above instead to be:
  • Width = phase0Bckgrnd.Width*4 - phase0Bckgrnd.Width/2
  • X = phase0Bckgrnd.X-Parent.X + phase0Bckgrnd.Width/4
This will get us much closer to what a user will expect.  The only things we are giving up is a slight amount of area to the left/right of the first & last columns that won't respond to clicks/slides.  

Could we fix this to be exactly correct?  Potentially yes.  We could extend the control both above/below the #'s we will allow the user to set, use some logical code to eliminate the ability to slide above/below, and put controls over the outer edges of this so it cannot be selected.  However, this is fairly close to what would be desired for an end-user.

Now, if we'd done a much larger # of Slider values (e.g. 0-100) and then did some logic for deciding if someone had drug it far enough for us to "snap" to the next column, then that would work.  But generally, that is where I abandon the "snap to" method and only snap the Text Input to the column when the mouse is released.  Plus, if you do it that way, then you could cause issues w/ people who might attempt to do this via keyboard controls.

For now, let's continue.

As a last step, we're going to hide the Slider completely.  Now we cannot use the Visible property here because we still want the user to interact with it.  Instead, we must make the control completely transparent.  For the Slider, change the following:
  • ShowValue = false
    • This stops the slider number from even appearing
  • RailFill = RGBA(128, 130, 133, 0)
    • The color doesn't really matter as the 0 makes this completely transparent
  • HandleFill = RGBA(255, 255, 255, 0)
  • ValueFill = RGBA(0, 18, 107, 0)
  • FocusedBorderThickness = 0

Now the Slider cannot be seen, but it still works.  If you run it now, you can both slide your item left/right or alternatively just click inside the column for a particular row and it will jump to that value.



Updating the Data

If you've been paying close attention (but I'd be pleased if you're just still awake & reading), then you may have noticed that we haven't actually SAVED any of the changes to the Slider or linked them to the original data source.

Fortunately, this one is very easy.  Select your Slider and make the following change:

  • OnChange = Update(peopleList,ThisItem,{Name:ThisItem.Name, Phase:phaseSlider.Value})
This is taking the value from our Slider, the other values from the Row we're editing, and writing them back to the original Data source.

Now you could instead keep this data in limbo and then offer your user the ability to write this back after hitting Save (a good idea) or hitting Cancel and resetting all of the controls.  

I'm going to leave that to you to finish on your own.

Final Thoughts

This is a complete PITA.  However, the reason to consider using this is that Drag/Drop controls are highly requested.  There are scenarios where people need a simpler interface that is not so difficult to navigate.  Keeping controls large and simple to touch/manipulate can be essential in such scenarios.

No comments:

Post a Comment

Because some d-bag is throwing 'bot posts at my blog I've turned on full Moderation. If your comment doesn't show up immediately then that's why.