This is part of a larger idea/prototype thought experiment.
In my blog post
Design Affordance Controls, I describe ways which I believe that a few components in ARIA/common
toolbox patterns are really design affordances that one would
like to choose to employ to sections of content. This page illustrates one
take on that, illustrating a <spicy-sections>
element.
<spicy-sections>
This element allows authors to provide normal sectioning content and offer optional affordances to be provided at different times (mostly, when media query conditions are true). The same content can also be presented as (and mean/act like) a series of independent collapses, a set of exclusive collapses (single select accordions/responsive tabs) and all manner of tabs. To grok the basic idea, it might help to check out the demo page first. This page assumes you have the basic idea and gets into details.
<spicy-sections>
<!-- any heading here -->
<h2>Stuff that would go in a summary</h2>
<!-- details content, with an optional container -->
<div>Stuff that would go below a summary</div>
<!-- possibly more pairs of headings and content -->
</spicy-sections>
Pretty much everything about this idea requires somehow tying a media query's state to the employment (or not) of some kind of UI Affordances. To do this requires somehow serializing this desire. While it is among the most critical aspects to solve if we attempt to standardize something, how we actually serialize it isn't as important for understanding the basic ideas or demonstrating uses - so here, we are punting a little bit until we can agree on other aspects. Therefore, the approach we've taken here may (probably 'will') bear little resemblance to an actual solution. .
This
is what works in the custom element, right now: One can express
it via an attribute
(mq-affordances
) or in CSS via a Custom Property
(--const-mq-affordances
). In both cases the value is serialized
as a MediaAffordancesList
. You can
read more about this serialization and see the many notes and
caveats
there, but the simplest form of this lets one express that they would like
sections to provide collapse
affordances when the media is
screen and the max-width is 800px like this:
[screen and (max-width: 800px)] collapse
Yes. When we are finally "there" this would be a pseudo-classes, in the
custom element version too via
Custom States.
The benefit of those is that only the element can set them. For now though
we are not there, and this uses attributes. When the affordance is
employed it sets the affordance
attribute. Note that this is
non reflective and setting it will only screw things up for you, so just
don't.
/* currently, here */
[affordance="collapse"] h3 {
/*style your heading when collapse affordances are employed */
}
/* future state of custom element */
:state(affordance-mode-collapse) h3 {
/*style your heading when collapse affordances are employed */
}
/* a native vision something like this... */
:affordance(collapse) h3 {
/*style your heading when collapse affordances are employed */
}
Some affordances have bits which are only present when the affordance itself is present.
The tab-bar
mode projects your headings into a single Tab Bar
container. This element exposes, conveniently enough, as
::part(tab-bar)
, just incase you want to style it. Inside
that is also a ::part(tab-list)
which contains the actual heading elements. Between these you can build
lots of different styles of tabs, overflow handlings and so on.
Both collapse
and exclusive-collapse
modes
project your heading's text into a button element which becomes a
disclosure button for the content. The button is exposed, conveniently
enough as ::part(disclosure-button)
on the heading when it is employed. The indicator is rendered with before
and after pseudo elements of the part, which is an svg data url background
that is rotated. You can override it. In CSS, these only render if there
is a content property, so you can simply set one to an empty string and
unset the other to remove it thereby moving the indicator to the left or
right... and so on.
Yes. You can use the
.observeAffordanceChange(callback)
to register a
callback when the affordances change. It is passed the newly employed
mode(s) (modes actually, but ignore that for now, it will always be a Set,
either empty if no affordances are employed or containing the name of the
one that is).
document
.querySelector('spicy-sections')
.observeAffordanceChange((matched) => {
console.log([...matched][0])
// logs undefined if no affordances are employed, or the name of the one that is
})
Aka, "shouldn't this just be a string"? Yes, probably... or at least the first argument to it should be a string. This is part of a base API surface I'm thinking about and the short answer is "it's a work in progress" and it's taking me too long to keep making changes and documenting something and then revisiting, so I'm just presenting what it is now.
.addEventListener
?Probably? It is this way mainly because this is just a custom element and using an event listener makes it seem like I've thought through the questions that that implies: bubbling, canceling and so on... I haven't, so this is what we have. It is pretty simplistic.
.affordanceState
A key idea to remember is that these elements contain two conceptual
'controllers' which maintain state independently - one that is
"checkbox-like" (independent states) and one that is "radiobutton-like"
(group state). These states always exist whether their
affordances are shown, or not. All APIs about states of affordances are
are available via .affordanceState
properties of either the container element, or headings.
Yes, the container element's .affordanceState
contains a
.currentMode
property tells you this.
document.querySelector('spicy-sections').affordanceState.currentMode
// returns one of: "", "exclusive", "non-exclusive" or undefined
Yes, the container element's .affordanceState
contains a
.current
property tells you this.
document.querySelector('spicy-sections').affordanceState.current
// returns one of: "", "collapse", "exclusive-collapse" "tab-bar" or undefined
The container element's .affordanceState
provides a
convenience .getLabels()
method which returns a true array
containing all of the labels. You can use this to programmatically access
the individual label's .affordanceState
properties and do all
manner of useful things...
let af = document.querySelector('spicy-sections').affordanceState;
// which are expanded on screen now?
af.getLabels().filter(l => l.affordanceState.expanded)
// which is expanded exclusively (shown or not)
af.getLabels().find(l => l.affordanceState.exclusiveExpanded)
// which are expanded non-exclusively (shown or not)
af.getLabels().filter(l => l.affordanceState.nonExclusiveExpanded)
The content associated with labels are their
.nextElementSibling
.
The default state of all non-exclusive affordances are not expanded
(collapsed). The default state of first exclusive affordances is expanded,
the others are not. You can modify this by setting the boolean
default-activate
attribute on relevant headings. This has the
effect of activating them from their default state (just imagine clicking
them if they were visible). In non-exclusive mode, this will cause them to
be initially expanded, in exclusive mode, the last one (if several are
marked) will be expanded.
Additionally, one can add a defaults-on-match
attribute to
force the provided defaults to be restored each time the affordances are
shown. This can be useful for patterns that always want to collapse a
panel like the side bar of a web site when the screen gets small to get it
out of the way.
Different controls do different things when they are "interacted with", but in all cases it is the label (heading) that is interacted with. For example, clicking the same heading on an exclusive control twice in sequence does nothing, it is radiobutton-like. But clicking a different one has an effect of both showing that, and hiding the others. Conversely, the non-exclusive collapse mode causes that element to toggle between the expanded or collapsed states. Because of this, we deal with 'activation' in the API of the currently deployed state, both setting and listening.
You can programmatically activate the currently employed affordance by
calling the generic .activate()
method.
document
.querySelector('spicy-sections h3')
.affordanceState
.activate()
A <spicy-sections>
is an example of am
element like this. However, it's not the only one, probably. I have
related needs sometimes, so it might be helpful to provide a base
abstraction. Here, a <spicy-sections>
extends a
MediaAffordancesElement
. This is a base class for element
which might claim to provide some affordances and can know when various
media conditions are met. You can
read more about all of the details of that
if you are interested.