Discussion:
[Design] MW UI Modals First Version & Concept
Shahyar Ghobadpour
2014-10-20 16:16:37 UTC
Permalink
Hey everyone,

We have developed a modal component for Flow, based on the design spec for
MediaWiki-UI modals/dialogs. Thus, we have named it *mw.Modal*.

A very basic and feature-incomplete version of this modal can be seen at:
http://en.wikipedia.beta.wmflabs.org/wiki/Talk:Flow (hover over ellipsis
icon in topic, click "hide")

This modal implements the design outlined here
<https://trello.com/c/OLnPx7M4/331-g-11-spike-build-modal-dialog-box-for-hide-delete-suppress>,
but automates a lot of the visual aspects of it based on the input you give
it.

Below is an almost-verbatim copy of the original email I sent to another
mailing list, (discussion seen here:
https://lists.wikimedia.org/mailman/private/e2/2014-September/002494.html
), detailing the general concepts and use:


*First, an example using event binding:*
// Instantiate mw.Modal
*var* modal = new mw.*Modal*();
// Open up a modal with HTML
modal.*open*( "<button>Bye!</button>" );
// Bubble click to wrapper, use it to close the parent modal of the target
$( modal.*getContentNode*() ).*bind*( 'click', 'button', function () {
mw.Modal.*close*( this ); } );
*Visuals (markup and CSS):*
As it stands, you can see the current designs for modals here
<https://trello.com/c/OLnPx7M4/331-g-11-spike-build-modal-dialog-box-for-hide-delete-suppress>,
with a screenshot of a Flow use case. This is being done with some very
simple markup:

flow-ui-modal

flow-ui-modal-layout

flow-ui-modal-heading* (optional)*

content + quick navigators* (automatic)*

flow-ui-modal-content

content


The CSS for this works back to IE6. It automatically centers on the
viewport, and scales up to max the viewport width/height, at which point
the modal would start showing scrollbar(s). No JS is needed for any of this.

*Automatic enhancement:*

- Shows title at top if it exists.
- Shows X on left of title to close if title exists.
- Shows ✓ form submission shortcut if title exists, and form with a
single primary button exists in content.


Caveat: This does not support off-center placement of modals (eg. placing a
modal by a specific element). For those use cases, I imagine *mw-ui-tooltip*
would be the solution, and it was written with that in mind.


*Use (JavaScript):*
A constructor would be called (modal = new mw.Modal( [ String name, ] [
Object settings ] )), which would give you access to control the flow of
your modal.
name may be omitted, but is simply there so you can easily determine which
modal is currently active with mw.Modal.getModal.
settings can have the following keys: open (same as modal.open), and title.

1. modal.*open*( [ Array|Object|Element|jQuery|String contents ] ) - You
can visually render the modal using this method. Accepts Element/jQuery, or
HTML String. Returns mw.Modal instance.
- Multi-step modals with an Array. You can pass [ Element, Element ]
to have two steps.
- Multi-step modals with an Object to have *named step keys*. Pass {
*steps*: [ 'first', 'second', 'foobar' ], *first*: Element, *second*:
Element, *foobar*: Element } for three steps.

2. modal.*close*() - Closes and destroys the given instance of
mw.Modal.
- *(Global) *mw.Modal.close( [ Element node ] ) - Closes the
currently-open modal (no args), or the modal in which node belongs.
Returns false if none, true if successfully destroyed.

3. modal.*go*( int|String to ) - For a multi-step modal, goes to a
specific step by number or name. Returns false if invalid step, mw.Modal
instance otherwise.
modal.*next*() and
modal.*prev*() - For a multi-step modal, goes to the previous or next
step, if any are left. Returns false if none, mw.Modal instance
otherwise.
- *(Global) *mw.Modal.*go*( int|String to, [ Element node ] ) - Go to
step on the currently-open modal (no args), or the modal in which node
belongs. Returns false if none, mw.Modal instance otherwise.
- *(Global) *mw.Modal.*next*( [ Element node ] ) - Next/prev step on
the currently-open modal (no args), or the modal in which node
belongs. Returns false if none, mw.Modal instance otherwise.

4. modal.*setTitle*( String title ) - Changes the title of the modal.

5. modal.*addSteps*( Array|Object|Element|jQuery|String contents ) -
Adds one or more steps, using the same arguments as modal.open. May
overwrite steps if any exist with the same key in Object mode. Returns
mw.Modal instance.

6. modal.*setStep*( int|String to, Element|jQuery|String contents ) -
Changes a given step. If String to does not exist in the list of steps,
throws an exception; int to always succeeds. If the given step is the
currently-active one, rerenders the modal contents. Returns mw.Modal
instance.
*Theoretically, you could use setStep to keep changing step 1 to create
a pseudo-multi-step modal.*

7. modal.*getSteps*() - Returns an Object with steps, and their
contents, eg. { *steps*: [ 1, 'foo', 'bar', 4 ], *1*: Element, '*foo*':
Element, '*bar*': Element, *4*: Element }
- *(Global)* mw.Modal.*getSteps*( [ Element node ] ) - Get an Array
of steps on the currently-open modal (no args), or the modal in which
node belongs. False on failure.

8. modal.*getNode*() - Returns the modal's wrapper Element, which
contains the header node and content node.
modal.*getContentNode*() - Returns the wrapping Element on which you can
bind bubbling events for your content.
- *(Global)* mw.Modal.*getContentNode*( [ Element node ] ) - Returns the
wrapping content Element on the currently-open modal (no args), or the
modal in which node belongs. False on failure.

9. modal.*setInteractiveHandler*( String name, Function callback ) -
See "inline event handlers" below. Some helper handlers for modals are
predefined, including "modalClose", "modalNavigate" (with
*data-mwui-modal-to* attribute), "modalPrev", and "modalNext".

An additional attribute, *data-mwui-interactive-target* is a jQuery
selector which allows you to point that event to interact with another
element, without having to put this selector in your JS. It supports an
unorthodox selector, the "closest parent" selector: "<".
eg. *data-mwui-interactive-target="< .foo .bar"* will find the closest
parent .foo, and then any nodes within .bar.

10. *(Global) *mw.Modal.*getModal*() - Returns an mw.Modal instance if
one is active, false otherwise.

11. modal.*getName*() - Returns the modal's name, as defined in
initialization.


*mw.Modal events:*
mw.Modal also inherits *ooJS' EventEmitter*. So, you may bind to individual
instances of mw.Modal, or even the global mw.Modal class itself. See wiki
<http://www.mediawiki.org/wiki/OOjs/Events>. Current events list:
*open*(mw.Modal
instance), *close*(mw.Modal instance, Element target), *navigate*(mw.Modal
instance, String nextStepKey), *render*(mw.Modal instance) (only triggered
on the first render of a given piece of content, not subsequent ones).
Returning *false* from any of these event callbacks will cancel the event
from continuing.


*Inline event callers:*
You can use special *data-* attributes to trigger/listen for events without
having to specifically bind to those elements. This allows you to write
more dynamic HTML, without having to adjust JavaScript when you change
elements or selectors. Some predefined helper handlers already exist (see
#8 above).

*Example:*

<button *data-mwui-interactive-handler*="*modalClose*">Close</button>
These event handling techniques are borrowed from the way Flow currently
"binds" events via markup. We do this, because we found a significant
amount of code was dedicated to finding elements with selectors in JS. Any
changes to markup required a change to the JS as well. This method allows
us to change the markup without changing the JS in many cases, as the
selectors are localized to the HTML. In addition, cumbersome parent
selecting is handled with our own "closest" selector (<).

In addition, we've been experimenting with an event forwarding mechanism
which "forwards" mouse & keyboard events from within the modal, out to the
element which triggered the modal to begin with (or any other arbitrary
node). This allows us to capture events within the modal, without having to
bind directly to it.


--Shahyar
Brion Vibber
2014-10-20 17:33:32 UTC
Permalink
Looks cool!

Quick note: it looks like keyboard navigation with 'tab' can still take you
to other text-entry fields etc in the background page and let you interact
with them on the keyboard while the modal's open.

Should these also open as a full-screen overlay style on small-screen
mobile devices (phones)? If so they may need to be fixed to handle
scrolling through anything that's taller than a couple lines of text, as on
small phones there's very little room once the keyboard's open.


(And as a general note for Flow UI, "Hide" is very ambiguous; does it hide
for just me or for everyone? Who is going to read the text I'm entering?)

-- brion

On Mon, Oct 20, 2014 at 9:16 AM, Shahyar Ghobadpour <
Post by Shahyar Ghobadpour
Hey everyone,
We have developed a modal component for Flow, based on the design spec for
MediaWiki-UI modals/dialogs. Thus, we have named it *mw.Modal*.
http://en.wikipedia.beta.wmflabs.org/wiki/Talk:Flow (hover over ellipsis
icon in topic, click "hide")
This modal implements the design outlined here
<https://trello.com/c/OLnPx7M4/331-g-11-spike-build-modal-dialog-box-for-hide-delete-suppress>,
but automates a lot of the visual aspects of it based on the input you give
it.
Below is an almost-verbatim copy of the original email I sent to another
https://lists.wikimedia.org/mailman/private/e2/2014-September/002494.html
*First, an example using event binding:*
// Instantiate mw.Modal
*var* modal = new mw.*Modal*();
// Open up a modal with HTML
modal.*open*( "<button>Bye!</button>" );
// Bubble click to wrapper, use it to close the parent modal of the target
$( modal.*getContentNode*() ).*bind*( 'click', 'button', function () {
mw.Modal.*close*( this ); } );
*Visuals (markup and CSS):*
As it stands, you can see the current designs for modals here
<https://trello.com/c/OLnPx7M4/331-g-11-spike-build-modal-dialog-box-for-hide-delete-suppress>,
with a screenshot of a Flow use case. This is being done with some very
flow-ui-modal
flow-ui-modal-layout
flow-ui-modal-heading* (optional)*
content + quick navigators* (automatic)*
flow-ui-modal-content
content
The CSS for this works back to IE6. It automatically centers on the
viewport, and scales up to max the viewport width/height, at which point
the modal would start showing scrollbar(s). No JS is needed for any of this.
*Automatic enhancement:*
- Shows title at top if it exists.
- Shows X on left of title to close if title exists.
- Shows ✓ form submission shortcut if title exists, and form with a
single primary button exists in content.
Caveat: This does not support off-center placement of modals (eg. placing
a modal by a specific element). For those use cases, I imagine
*mw-ui-tooltip* would be the solution, and it was written with that in
mind.
*Use (JavaScript):*
A constructor would be called (modal = new mw.Modal( [ String name, ] [
Object settings ] )), which would give you access to control the flow of
your modal.
name may be omitted, but is simply there so you can easily determine
which modal is currently active with mw.Modal.getModal.
settings can have the following keys: open (same as modal.open), and title
.
1. modal.*open*( [ Array|Object|Element|jQuery|String contents ] ) -
You can visually render the modal using this method. Accepts
Element/jQuery, or HTML String. Returns mw.Modal instance.
- Multi-step modals with an Array. You can pass [ Element, Element ]
to have two steps.
- Multi-step modals with an Object to have *named step keys*. Pass {
Element, *foobar*: Element } for three steps.
2. modal.*close*() - Closes and destroys the given instance of
mw.Modal.
- *(Global) *mw.Modal.close( [ Element node ] ) - Closes the
currently-open modal (no args), or the modal in which node belongs.
Returns false if none, true if successfully destroyed.
3. modal.*go*( int|String to ) - For a multi-step modal, goes to a
specific step by number or name. Returns false if invalid step,
mw.Modal instance otherwise.
modal.*next*() and
modal.*prev*() - For a multi-step modal, goes to the previous or next
step, if any are left. Returns false if none, mw.Modal instance
otherwise.
- *(Global) *mw.Modal.*go*( int|String to, [ Element node ] ) - Go
to step on the currently-open modal (no args), or the modal in which
node belongs. Returns false if none, mw.Modal instance otherwise.
- *(Global) *mw.Modal.*next*( [ Element node ] ) - Next/prev step
on the currently-open modal (no args), or the modal in which node
belongs. Returns false if none, mw.Modal instance otherwise.
4. modal.*setTitle*( String title ) - Changes the title of the modal.
5. modal.*addSteps*( Array|Object|Element|jQuery|String contents ) -
Adds one or more steps, using the same arguments as modal.open. May
overwrite steps if any exist with the same key in Object mode. Returns
mw.Modal instance.
6. modal.*setStep*( int|String to, Element|jQuery|String contents ) -
Changes a given step. If String to does not exist in the list of
steps, throws an exception; int to always succeeds. If the given step
is the currently-active one, rerenders the modal contents. Returns mw.Modal
instance.
*Theoretically, you could use setStep to keep changing step 1 to
create a pseudo-multi-step modal.*
7. modal.*getSteps*() - Returns an Object with steps, and their
Element, '*bar*': Element, *4*: Element }
- *(Global)* mw.Modal.*getSteps*( [ Element node ] ) - Get an Array
of steps on the currently-open modal (no args), or the modal in which
node belongs. False on failure.
8. modal.*getNode*() - Returns the modal's wrapper Element, which
contains the header node and content node.
modal.*getContentNode*() - Returns the wrapping Element on which you
can bind bubbling events for your content.
- *(Global)* mw.Modal.*getContentNode*( [ Element node ] ) - Returns
the wrapping content Element on the currently-open modal (no args), or the
modal in which node belongs. False on failure.
9. modal.*setInteractiveHandler*( String name, Function callback ) -
See "inline event handlers" below. Some helper handlers for modals are
predefined, including "modalClose", "modalNavigate" (with
*data-mwui-modal-to* attribute), "modalPrev", and "modalNext".
An additional attribute, *data-mwui-interactive-target* is a jQuery
selector which allows you to point that event to interact with another
element, without having to put this selector in your JS. It supports an
unorthodox selector, the "closest parent" selector: "<".
eg. *data-mwui-interactive-target="< .foo .bar"* will find the closest
parent .foo, and then any nodes within .bar.
10. *(Global) *mw.Modal.*getModal*() - Returns an mw.Modal instance if
one is active, false otherwise.
11. modal.*getName*() - Returns the modal's name, as defined in
initialization.
*mw.Modal events:*
mw.Modal also inherits *ooJS' EventEmitter*. So, you may bind to
individual instances of mw.Modal, or even the global mw.Modal class itself. See
*open*(mw.Modal instance), *close*(mw.Modal instance, Element target),
*navigate*(mw.Modal instance, String nextStepKey), *render*(mw.Modal
instance) (only triggered on the first render of a given piece of
content, not subsequent ones). Returning *false* from any of these event
callbacks will cancel the event from continuing.
*Inline event callers:*
You can use special *data-* attributes to trigger/listen for events
without having to specifically bind to those elements. This allows you to
write more dynamic HTML, without having to adjust JavaScript when you
change elements or selectors. Some predefined helper handlers already exist
(see #8 above).
*Example:*
<button *data-mwui-interactive-handler*="*modalClose*">Close</button>
These event handling techniques are borrowed from the way Flow currently
"binds" events via markup. We do this, because we found a significant
amount of code was dedicated to finding elements with selectors in JS. Any
changes to markup required a change to the JS as well. This method allows
us to change the markup without changing the JS in many cases, as the
selectors are localized to the HTML. In addition, cumbersome parent
selecting is handled with our own "closest" selector (<).
In addition, we've been experimenting with an event forwarding mechanism
which "forwards" mouse & keyboard events from within the modal, out to the
element which triggered the modal to begin with (or any other arbitrary
node). This allows us to capture events within the modal, without having to
bind directly to it.
--Shahyar
_______________________________________________
Design mailing list
https://lists.wikimedia.org/mailman/listinfo/design
Shahyar Ghobadpour
2014-10-20 17:39:20 UTC
Permalink
Yeah, I may have to bind the keydown for tab to prevent that from
happening. The modal attempts to bring focus to the modal (or its contents)
at first, so pressing tab should initially bring focus to items in that
modal. Keep pressing, and you end up back on the regular page, though.
Haven't really thought that through, so it's worth looking into.

These full-screen overlays automatically resize. They have a minimum size,
but they scale up to take up almost the whole viewport when needed, and
beyond that they they become scrollable (with overflow: auto). This is most
important on mobile.

(As for Flow, we are somewhat aware of that and have been discussing it.)

--Shahyar
Post by Brion Vibber
Looks cool!
Quick note: it looks like keyboard navigation with 'tab' can still take
you to other text-entry fields etc in the background page and let you
interact with them on the keyboard while the modal's open.
Should these also open as a full-screen overlay style on small-screen
mobile devices (phones)? If so they may need to be fixed to handle
scrolling through anything that's taller than a couple lines of text, as on
small phones there's very little room once the keyboard's open.
(And as a general note for Flow UI, "Hide" is very ambiguous; does it hide
for just me or for everyone? Who is going to read the text I'm entering?)
-- brion
On Mon, Oct 20, 2014 at 9:16 AM, Shahyar Ghobadpour <
Post by Shahyar Ghobadpour
Hey everyone,
We have developed a modal component for Flow, based on the design spec
for MediaWiki-UI modals/dialogs. Thus, we have named it *mw.Modal*.
http://en.wikipedia.beta.wmflabs.org/wiki/Talk:Flow (hover over ellipsis
icon in topic, click "hide")
This modal implements the design outlined here
<https://trello.com/c/OLnPx7M4/331-g-11-spike-build-modal-dialog-box-for-hide-delete-suppress>,
but automates a lot of the visual aspects of it based on the input you give
it.
Below is an almost-verbatim copy of the original email I sent to another
https://lists.wikimedia.org/mailman/private/e2/2014-September/002494.html
*First, an example using event binding:*
// Instantiate mw.Modal
*var* modal = new mw.*Modal*();
// Open up a modal with HTML
modal.*open*( "<button>Bye!</button>" );
// Bubble click to wrapper, use it to close the parent modal of the target
$( modal.*getContentNode*() ).*bind*( 'click', 'button', function () {
mw.Modal.*close*( this ); } );
*Visuals (markup and CSS):*
As it stands, you can see the current designs for modals here
<https://trello.com/c/OLnPx7M4/331-g-11-spike-build-modal-dialog-box-for-hide-delete-suppress>,
with a screenshot of a Flow use case. This is being done with some very
flow-ui-modal
flow-ui-modal-layout
flow-ui-modal-heading* (optional)*
content + quick navigators* (automatic)*
flow-ui-modal-content
content
The CSS for this works back to IE6. It automatically centers on the
viewport, and scales up to max the viewport width/height, at which point
the modal would start showing scrollbar(s). No JS is needed for any of this.
*Automatic enhancement:*
- Shows title at top if it exists.
- Shows X on left of title to close if title exists.
- Shows ✓ form submission shortcut if title exists, and form with a
single primary button exists in content.
Caveat: This does not support off-center placement of modals (eg. placing
a modal by a specific element). For those use cases, I imagine
*mw-ui-tooltip* would be the solution, and it was written with that in
mind.
*Use (JavaScript):*
A constructor would be called (modal = new mw.Modal( [ String name, ] [
Object settings ] )), which would give you access to control the flow of
your modal.
name may be omitted, but is simply there so you can easily determine
which modal is currently active with mw.Modal.getModal.
settings can have the following keys: open (same as modal.open), and title.
1. modal.*open*( [ Array|Object|Element|jQuery|String contents ] ) -
You can visually render the modal using this method. Accepts
Element/jQuery, or HTML String. Returns mw.Modal instance.
- Multi-step modals with an Array. You can pass [ Element, Element
] to have two steps.
- Multi-step modals with an Object to have *named step keys*. Pass {
*steps*: [ 'first', 'second', 'foobar' ], *first*: Element,
*second*: Element, *foobar*: Element } for three steps.
2. modal.*close*() - Closes and destroys the given instance of
mw.Modal.
- *(Global) *mw.Modal.close( [ Element node ] ) - Closes the
currently-open modal (no args), or the modal in which node
belongs. Returns false if none, true if successfully destroyed.
3. modal.*go*( int|String to ) - For a multi-step modal, goes to a
specific step by number or name. Returns false if invalid step,
mw.Modal instance otherwise.
modal.*next*() and
modal.*prev*() - For a multi-step modal, goes to the previous or next
step, if any are left. Returns false if none, mw.Modal instance
otherwise.
- *(Global) *mw.Modal.*go*( int|String to, [ Element node ] ) - Go
to step on the currently-open modal (no args), or the modal in which
node belongs. Returns false if none, mw.Modal instance otherwise.
- *(Global) *mw.Modal.*next*( [ Element node ] ) - Next/prev step
on the currently-open modal (no args), or the modal in which node
belongs. Returns false if none, mw.Modal instance otherwise.
4. modal.*setTitle*( String title ) - Changes the title of the modal.
5. modal.*addSteps*( Array|Object|Element|jQuery|String contents ) -
Adds one or more steps, using the same arguments as modal.open. May
overwrite steps if any exist with the same key in Object mode. Returns
mw.Modal instance.
6. modal.*setStep*( int|String to, Element|jQuery|String contents ) -
Changes a given step. If String to does not exist in the list of
steps, throws an exception; int to always succeeds. If the given step
is the currently-active one, rerenders the modal contents. Returns mw.Modal
instance.
*Theoretically, you could use setStep to keep changing step 1 to
create a pseudo-multi-step modal.*
7. modal.*getSteps*() - Returns an Object with steps, and their
Element, '*bar*': Element, *4*: Element }
- *(Global)* mw.Modal.*getSteps*( [ Element node ] ) - Get an
Array of steps on the currently-open modal (no args), or the modal in which
node belongs. False on failure.
8. modal.*getNode*() - Returns the modal's wrapper Element, which
contains the header node and content node.
modal.*getContentNode*() - Returns the wrapping Element on which you
can bind bubbling events for your content.
- *(Global)* mw.Modal.*getContentNode*( [ Element node ] ) - Returns
the wrapping content Element on the currently-open modal (no args), or the
modal in which node belongs. False on failure.
9. modal.*setInteractiveHandler*( String name, Function callback ) -
See "inline event handlers" below. Some helper handlers for modals are
predefined, including "modalClose", "modalNavigate" (with
*data-mwui-modal-to* attribute), "modalPrev", and "modalNext".
An additional attribute, *data-mwui-interactive-target* is a jQuery
selector which allows you to point that event to interact with another
element, without having to put this selector in your JS. It supports an
unorthodox selector, the "closest parent" selector: "<".
eg. *data-mwui-interactive-target="< .foo .bar"* will find the
closest parent .foo, and then any nodes within .bar.
10. *(Global) *mw.Modal.*getModal*() - Returns an mw.Modal instance
if one is active, false otherwise.
11. modal.*getName*() - Returns the modal's name, as defined in
initialization.
*mw.Modal events:*
mw.Modal also inherits *ooJS' EventEmitter*. So, you may bind to
individual instances of mw.Modal, or even the global mw.Modal class itself. See
*open*(mw.Modal instance), *close*(mw.Modal instance, Element target),
*navigate*(mw.Modal instance, String nextStepKey), *render*(mw.Modal
instance) (only triggered on the first render of a given piece of
content, not subsequent ones). Returning *false* from any of these
event callbacks will cancel the event from continuing.
*Inline event callers:*
You can use special *data-* attributes to trigger/listen for events
without having to specifically bind to those elements. This allows you to
write more dynamic HTML, without having to adjust JavaScript when you
change elements or selectors. Some predefined helper handlers already exist
(see #8 above).
*Example:*
<button *data-mwui-interactive-handler*="*modalClose*">Close</button>
These event handling techniques are borrowed from the way Flow currently
"binds" events via markup. We do this, because we found a significant
amount of code was dedicated to finding elements with selectors in JS. Any
changes to markup required a change to the JS as well. This method allows
us to change the markup without changing the JS in many cases, as the
selectors are localized to the HTML. In addition, cumbersome parent
selecting is handled with our own "closest" selector (<).
In addition, we've been experimenting with an event forwarding mechanism
which "forwards" mouse & keyboard events from within the modal, out to the
element which triggered the modal to begin with (or any other arbitrary
node). This allows us to capture events within the modal, without having to
bind directly to it.
--Shahyar
_______________________________________________
Design mailing list
https://lists.wikimedia.org/mailman/listinfo/design
_______________________________________________
Design mailing list
https://lists.wikimedia.org/mailman/listinfo/design
Loading...