[whatwg] Styling <details>

Hi,
   We've been experimenting with the styling of the details element, 
trying to figure out the most sensible way style it.  We have tried to 
find a solution that behaves the way authors expect, provides for easy 
restyling by authors and avoiding the troubles associated with magic 
styles that can't be expressed in CSS.

The rendering section of the spec is currently very inadequate and does 
not describe accurate styles.  Also, the sample XBL binding given in the 
XBL 2.0 draft is also inadequate for a number of reasons.


== Requirements ==

In designing the solution, we have a number of requirements that we are 
trying to meet as best we can.

1. The disclosure triangle must be styleable by authors, either to 
replace with their own icon, remove it entirely, or possibly adjust 
other common styles.

2. Styling the disclosure triangle should not require complicated hacks 
with margins, padding or otherwise, to hide the default disclosure icon 
and replace with a custom icon.

3. The default styles that apply directly to the details and summary 
elements must be quite simple, such as display, margin and/or padding.

4. The styles applied to the elements in the shadow tree must not have 
significant adverse effects on the details or summary elements, nor the 
surrounding content. (We should avoid floating or other styles that may 
give unexpected results in certain conditions.)

5. If authors change the 'display' style of either the details or 
summary elements (e.g. inline, table-cell, etc.), the result should be 
sensible, and not have any unexpected results caused by the styling of 
the shadow tree. The binding template must not introduce extra 
whitespace between elements that would affect the rendering in such cases.

6. We cannot require, nor expect, authors to use XBL to restyle these 
elements. (We aren't actually implementing it with XBL, but we have been 
discussing it in terms of XBL for future compatibility with it)

7. The content and styling of the shadow tree must not adversely affect 
the use of ::before and ::after pseudo-elements applied to either 
details or summary. (Note: Chromium's <details> implementation has some 
strange handling for details::before, preferring to render it after the 
summary instead of before.)

8. The special summary styling, including the placement of the 
disclosure widget, should only apply to the first child summary element 
of the details.  Subsequent summary elements must not be rendered in 
unexpected ways.

9. The default action of opening/closing the details should only apply 
when the user clicks on either the summary text or the disclosure 
triangle.  It should not apply if the user clicks on the other content 
within the details element.

10. The summary element must be focussable by default and keyboard 
activation must be possible.  The focus ring should be drawn around the 
summary element and/or the disclosure triangle, and not the entire 
details element.

11. The disclosure triangle and any applicable margins and padding must 
render on the opposite side and point the opposite direction for RTL 
languages.

12. It is preferred to reuse as much existing CSS styles as possible to 
achieve the effects, avoiding unnecessary creation of special properties 
or values without a good reason.


== Problems with the Spec ==

*Rendering*

There are a number of problems with the way in which the rendering 
section describes how to render details [2].


> When the details binding applies to a details element, the element is
> expected to render as a 'block' box with its 'padding-left' property
> set to '40px' for left-to-right elements (LTR-specific) and with its
> 'padding-right' property set to '40px' for right-to-left elements.

> The first container is expected to contain at least one line box, and
> that line box is expected to contain a disclosure widget (typically
> a triangle), horizontally positioned within the left padding of the
> details element.

According to these requirements, the details element should be rendered 
something like this:

   +---+-------------------------+
   | > | Details                 |
   |   +-------------------------|
   |   | The content goes here   |
   +-----------------------------+

This is analogous to how it works for ul and ol.  However, this creates 
a substantial amount of padding on the left which authors are likely to 
want to remove or otherwise significantly reduce in most cases.

An alternative approach is to apply a small amount of margin or padding 
to the summary element alone, just enough to render the disclosure 
triangle within, leaving the remaining content unindented.  In this 
case, a margin or padding of 1em would be a more reasonable size.  40px 
is too much

   +---+-------------------------+
   | > | Details                 |
   +---+-------------------------|
   | The content goes here       |
   +-----------------------------+

Note that Chromium's current implementation has an appearance visually 
like this, and it more closely matches similar native mechanisms on 
platforms that do not indent the content below the disclosure widget. 
See, e.g. the screenshot of the Mac Info dialog from the spec.

http://images.whatwg.org/sample-details-2.png

We think that this latter style is a better default style to aim for and 
would like to get feedback regarding this issue.  In any case, it should 
be trivial for authors to achieve either effect by adjusting the margin 
and/or padding of the summary and details element.


*The Effect of the Shadow Tree*

> The element's shadow tree is expected to take the element's first
> child summary element, if any, and place it in a first 'block' box
> container, and then take the element's remaining descendants, if any,
> and place them in a second 'block' box container.

For reference, this maps to this sample binding from the XBL draft:

<template allow-selectors-through="true">
  ...
  <div>
   <div><content includes="summary:first-child">Details...</content></div>
   <div data-state="hidden" id="container"><content></content></div>
  </div>
</template>

The problem with this design is that it inserts new layout boxes into 
the design that the author has no control over,  which limits the 
ability of authors to restyle summary and details.  (This issue will be 
discussed in more detail later)

> The second container is expected to have its 'overflow' property set
> to 'hidden'. When the details element does not have an open
> attribute, this second container is expected to be removed from the
> rendering.

Setting 'overflow: hidden;' will not have any effect on the second 
container without there being a specified height.  But it's not entirely 
clear what the desired effect of that requirement is.

If the author specifies a height on the details element, then the 
overflow should be handled by the 'overflow' property that is applied to 
it by the author, rather than on any shadow content to which they have 
no access.  Setting 'overflow' to 'hidden' on the shadow content would 
never actually cause anything to be hidden under any conditions.


*Activation Behaviour*

The spec does not clearly define activation behaviour.  Ideally, the 
default action of opening and closing should only occur when the user 
clicks the summary element or the disclosure triangle.  it should not 
occur when clicking elsewhere.

In future implementations that support XBL, it should be possible for 
authors to change the binding of the details element for layout 
purposes, while still retaining the default action associated with the 
summary element.  Therefore, it seems unwise for this functionality to 
be implemented via a binding, and would prefer instead that it be 
specified as the summary element's activation behaviour.

It should also be possible for this default action to be cancelled with 
evt.preventDefault() called within an event listener, which will allow 
scripts or bindings to provide their own custom behaviour if desired.


== Problems Encountered While Developing a Solution ==

After many experiments with different styles and simulated bindings, we 
encountered numerous problems that made it difficult to achieve the 
desired outcome using bindings.


*Interfering Shadow Elements*

When the binding template used two block level elements to render the 
content, like so:

<template>
   <div><content includes="summary:first-of-type">Details</content></div>
   <div id="content"><content></content></div>
</template>

This produced undesirable renderings in a number of cases where authors 
specify the following styles:

   details, summary { display: inline; }

The divs in the binding still render as block, causing this inline style 
to have little apparent visual effect.  At first we thought we could 
address this by making those elements inline (using <span> instead), but 
then we still found we ran into trouble with this case:

   details { display: table; }
   details>summary, details>div { display: table-cell; }

Assume this markup:

   <details>
     <summary>Summary</summary>
     <div>Content</div>
   </details>

Authors would expect the summary and div, as siblings, to render as 
table cells side-by-side.


   +===========================+
   | ......................... |
   | : +---------+---------+ : |
   | : | Summary | Content | : |
   | : +---------+---------+ : |
   | :.......................: |
   +===========================+

   ... : Anonymous table row box
   === : Table box (details element)
   --- : Table cell boxes

Yet because of the block styled divs or inline styled spans in the 
binding, this would instead result in separate anonymous table and 
table-row boxes generated around each element, rendered inside the block 
boxes.  Thus, they would still be rendered one on top of the other 
instead of side by side.

   +=================++=================+
   | ............... || ............... |
   | : +---------+ : || : +---------+ : |
   | : | Summary | : || : | Content | : |
   | : +---------+ : || : +---------+ : |
   | :.............: || :.............: |
   +=================++=================+

   === : Inline boxes from the binding (<span> elements)
   ... : Anonymous table and table row boxes
   --- : Table cell boxes

(I simplified the diagram to leave out the details element table box, 
and the anonymous table-row and table-cell boxes that would be generated 
around all of that)

This effectively makes two independent tables, rather than a single 
table with two cells.  Depending on the content or other styles, it 
could, for example, result in the two cells unexpectedly having 
different heights or wrapping around onto separate lines.

The result doesn't comply with requirements #4 or #5, and is thus 
unsuitable for our needs.  On its own, it would require authors to use 
XBL to create their own binding with their own layout template in order 
to effectively restyle the elements, which doesn't comply with 
requirement #6.

We thus determined that we needed to find a solution that either gave 
authors access to this shadow content from CSS, or which didn't generate 
any additional layout boxes around the summary or content.


*Rendering the Disclosure Triangle*

Another problem was trying to find a suitable method that would allow us 
to render the disclosure triangle without internal magic, leaving it 
styleable by authors.

We want to avoid a situation like that with fieldset and legend, where 
authors are severely limited in their ability to restyle the elements. 
We also want to avoid an implementation like that in current Chromium 
builds, where some internal magic has been used to insert the disclosure 
triangle in a way that seems impossible to remove, and which breaks when 
various styles are applied to details and/or summary.

The first approach we tried was to use the binding to insert a box into 
the rendering, and allow it to be addressed by a pseudo-element.

Our first thought was to reuse ::marker from the CSS3 Lists draft, but 
didn't think it was sensible to hijack that from list-item boxes for 
this purpose.  So we experimented with a different name instead.

   <binding id="summary">
     <template>
       <span pseudo="-o-disclosure"></span>
       <content>Details</content>
     </template>
   </binding>

This would be applied to the summary element.

   details>summary:first-of-type { binding: details.xml#summary; }

We would then use the 'content' property to insert a suitable character 
glyph or image to render the disclosure triangle.

   details>summary::-o-disclosure {
     content: "?"; /* U+25B8 BLACK RIGHT-POINTING SMALL TRIANGLE */
     margin-left: 1em;
     width: 1em;
   }
   details[open]>summary::-o-disclosure {
     content: "?"; /* U+25BE BLACK DOWN-POINTING SMALL TRIANGLE */
   }
   summary { padding-left: 1em; }

The negative margin is used to push the disclosure widget to the outside 
of the box and then compensate for that to prevent it going too far by 
adding padding to the summary element.  This had the effect of rendering 
the disclosure triangle within the padding of the summary.

i.e. The resulting layout looked like this:

   +---+-------------------------+
   | ? | Details                 |
   +---+-------------------------|

This worked well in some cases, but we ran into troubles when other 
pseudo-elements were used as well.

   summary::before { content: "x"; }

Because of the way ::before is defined, it would still be inserted 
before the ::-o-disclosure box, and due to the negative margin, the 
disclosure triangle would render on top of it.  This was unacceptable, 
and so we needed to find a way to make sure the disclosure widget always 
rendered before any ::before content.

One possibility would be to insert a new element into the binding after 
the marker, like so:

   <template>
     <span pseudo="-o-disclosure"></span>
     <span pseudo="before"></span>
     <content>Details</content>
   </template>

But XBL as defined does not allow for the ::before pseduo-element to be 
remapped like that.

We tried various other permutations of templates, moving elements around 
in the binding to position the ::-o-disclosure box outside of the 
summary box.  Each presented its own set of layout problems under 
certain circumstances.

For example, there were unexpected consequences when the summary element 
wrapped around, causing the summary text to flow underneath the 
disclosure triangle.  Ideally, the rendering should look like this:

   ? This is a summary that wraps
     around onto multiple lines

But in some cases, the result looked this this:

   ? This is a summary that wraps
   around onto multiple lines

In extreme cases, it because possible for the disclosure triangle to 
become unintentionally visually separated from the summary.

e.g. With this author style applied:

   summary { display: inline; }

The following could happen in some of our experiments.  Assume this markup:

   <div>line box with content before
     <details><summary>Summary wrapped to the next line</summary>
   </div>

The rendering could place the disclosure triangle in a separate line box 
from the summary, which is undesirable.

   line box with content before. ?
   Summary wrapped to the next line

Other experiments we tried resulted in other similarly unacceptable 
renderings; used unreasonable amounts of CSS; or used fragile styles 
like floats that could unexpectedly affect surrounding content, which we 
really want to avoid. and so ultimately, we decided that we could not 
use bindings effectively to insert such an element to render the 
disclosure triangle, and we needed another solution.


*Hiding and Showing the Content*

Given that there is no explicit container around the content of details 
excluding the summary, we still needed the binding to contain an element 
that we could style with display:none; when in the closed state.

   <template allow-selectors-through="true">
     <style scoped>
       :bound-element:not([open]) #content { display: none; }
     </style>
     <content includes=":bound-element>summary:first-of-type"></content>
     <span id="content"><content></content></span>
   </template>

This method would effectively hide and show the content depending on the 
state of the open attribute, exactly as specified.  But then we still 
ran into the trouble described above in the display:table-cell; case 
described earlier.

While it was useful to have the element there in the closed 
(display:none;) state, it became a nuisance in the open state.  We 
therefore needed to find a solution that would make the shadow element 
disappear from the layout entirely when it wasn't needed in the open 
state, yet still keep it around for the closed state, or to somehow make 
it accessible to authors for styling.


== Proposed Solutions ==

*Rendering the Disclosure Triangle*

We eventually found that we could make use of display: list-item; and a 
custom list-style to render the disclosure triangle beside the summary.

This approach has a number of advantages over the previous attempts we 
tried using bindings.

* It allows us to take advantage of existing infrastructure
* It handles the ::before pseudo-element case correctly, which wasn't
   handled well with our previous experiments.
* It also gives authors a familiar and easy way to provide custom icons
   using 'list-style'.
* In the future, it should also give us the ::marker pseudo-element
   for free, if and when that gets implemented.

One limitation we discovered with this approach is that Opera currently 
does not register click events when the user clicks on the bullet when 
'list-style-position' is 'outside'.  We prefer to keep this as 'outside' 
rather than 'inside' so that the disclosure triangle is rendered in the 
margin of the summary element.

We consider this to be a bug in our implementation, since the ::marker 
should be considered to be a descendant of the element (just like 
::before).  Other implementations in Gecko and WebKit do register click 
events, but they limit the clickable area to the small region where the 
bullet is rendered.  It may be better if the clickable area was made 
larger so that it is easier for users to click.

To render this, the following CSS should be applied by the UA stylesheet.

   details {
     display: block;
   }

   details>summary:first-of-type {
     display: list-item;
     margin-left: 1em; /* LTR-specific: use 'margin-right' for rtl 
elements */
     list-style-type: -o-disclosure-closed;
   }

   details[open]>summary:first-of-type  {
     list-style-type: -o-disclosure-open;
   }

Variations:

* It is also possible for us to specify 40px padding on the details
   element as currently specified, rather than the 1em margin on the
   summary.

* As an alternative to defining new 'list-style-type' values, it is
   also possible that we could achieve the effects using
   'list-style-image'. i.e.

     list-style-image: url(disclosure-closed.svg);
     list-style-image: url(disclosure-open.svg);

   However, 'list-style-type' has some advantages
   to consider:

   - 'list-style-type' allows the disclosure icons to be handled like
     existing list bullets (disc, circle, etc.), making them available
     for use on lists too.
   - The colour of the list-style-type is inherited from the element,
     the 'list-style-image' is not.


I have created a simulated version with JavaScript to show the visual 
layout of this approach.  This demo uses 'list-style-image' and works 
best in Opera or Firefox 4.  WebKit does not support SVG images for 
'list-style-image', and so it renders the default disc bullet instead.

It uses tabindex="0" to allow keyboard focussing. In Opera, the click 
event fires when Enter is pressed while focussed, demonstrating keyboard 
accessibility.  In Opera, due to the aforementioned bug, clicking the 
disclosure triangle won't work. Click the summary text instead, which 
works in all browsers.

http://lachy.id.au/dev/2011/details.html


*Showing and Hiding the Content*

There are three possible solutions that we have considered to address 
these issues.

1. Define a new special 'display' type that means the element generates
    no layout box itself, but still renders the content.

2. Dynamically change the binding to add and remove the shadow element
    as needed based on the open/closed state of the details element.

3. Define a new pseudo-element specifically for addressing the content
    area.


*New 'display' Type*

The proposal is to introduce a new special value for 'display' that 
means to not generate any layout box for the element, but still render 
its contents. i.e. Behave as if the element weren't there for layout 
purposes.

For this, we came up with:

   display: transparent;

In theory, this would allow the binding to change the 'display' of the 
the shadow element from 'none' in the closed state to 'transparent' in 
the open state, thus hiding and showing the content as required.

   <template allow-selectors-through="true">
     <style scoped>
       #content { display: none; }
       :bound-element[open] #content { display: transparent; }
     </style>
     <content includes=":bound-element>summary:first-of-type"></content>
     <span id="content"><content></content></span>
   </template>

Advantages:

This is a very general purpose solution which I suspect will be useful 
in many other bindings to solve similar problems.

Limitations:

A problem with this approach is that, depending on how existing CSS 
layout implementations work, it may introduce some complexity or 
implementation difficulties.  In particular, our layout developers 
expressed concern that there might be some internal implementation 
complexities introduced by such a feature.

However, as this is an internal implementation issue, it's not clear if 
such concerns would apply generally to all implementations, or if other 
implementations wouldn't have any significant difficulty with it.


*Dynamically Changing the Binding*

The next approach we came up with is to apply separate bindings based on 
the state of the details element.  That is, have one binding for the 
open state that rendered the content without any added elements in the 
shadow tree, and another for the closed state that hid the remaining 
content.

For this, we designed these bindings:

   <binding id="details-closed">
     <template allow-selectors-through="true">
       <content 
includes=":bound-element>summary:first-of-type"><summary>Details</summary></content>
       <span style="display:none;"><content></content></span>
     </template>
   </binding>

   <binding id="details-open">
     <template allow-selectors-through="true">
       <content 
includes=":bound-element>summary:first-of-type"><summary>Details</summary></content>
       <content></content>
     </template>
   </binding>


The first uses a span element in the shadow tree to hide the content, 
the latter simply includes the content directly without any surrounding 
span element.

The User Agent style sheet needed to apply these, including rendering 
the disclosure triangle, looks like this:


   details {
     display: block;
     binding: url(details.xml#details-closed);
   }

   details[open] {
     binding: url(details.xml#details-open);
   }

   details>summary:first-of-type {
     display: list-item;
     margin-left: 1em; /* margin-right for RTL */
     list-style-type: -o-disclosure-closed;
     binding: url(details.xml#summary);
   }

   details[open]>summary:first-of-type  {
     list-style-type: -o-disclosure-open;
   }

(This CSS is the same as before, but with added bindings)

With this approach, given an implementation that does actually use XBL, 
it would mean that the bindings would be attached and detached as the 
state of the details changes.

To illustrate how this works, I'll show what the shadow tree should look 
like when applied to a simple details/summary example:

   <details>
     <summary>Summary</summary>
     <p>Content</p>
   </details>

In the closed state, the shadow tree should look like this:

   details
   |
   +-- template
       |
       +-- content includes="summary"
       |   |
       |   +-- Summary
       |       |
       |       +-- Details
       |
       +-- span style="display:none;"
           |
           +-- content

This results in a final flattened tree that looks like this:

   details
   |
   +-- summary
   |
   +-- span style="display:none;"
       |
       +-- p


In the open state, the shadow tree should look like this:

   details
   |
   +-- template
       |
       +-- content includes="summary"
       |   |
       |   +-- Summary
       |       |
       |       +-- Details
       |
       +-- content

This results in a final flattened tree that looks like this:

   details
   |
   +-- summary
   |
   +-- p

Limitations:

This approach, however, may introduce some significant overhead as the 
bindings are attached and detached.  It also means that if an author 
wishes to provide their own binding in a future XBL-supporting 
implementation, they would have to override the binding for both states.


*New Pseudo-Elements*

The final solution is to introduce special new pseudo-elements 
specifically for use with details that would surround the content, 
excluding the summary.  This could be represented in the binding as follows:

   <binding id="details-closed">
     <template allow-selectors-through="true">
       <content 
includes=":bound-element>summary:first-of-type"><summary>Details</summary></content>
       <span id="content" pseudo="-o-content"><content></content></span>
     </template>
   </binding>

UA styles could then be used to hide and show the content as needed

   details::-o-content {
     display: none;
   }
   details[open]::-o-content {
     display: block;
   }

Limitations:

This requires the creation of a new element-specific pseudo-element 
specifically for use with details, which is not as nice as a more 
general purpose solution that can be applied to other situations.  It 
also doesn't really address the problem directly, but instead merely 
provides authors with a workaround.


== Open Issues ==

1. The summary should be focusable for keyboard navigation and 
activation.  This can't use tabindex in the binding, but rather should 
be handled natively by the implementation like links or form controls. 
For consistency, it needs to be possible to override if the author sets 
<summary tabindex="-1">.

2. Should the default "Details" string change based on the user's 
browser language, the page's language, or not change at all?

3. As stated earlier, we would like feedback regarding padding/margin issue.

[1] http://dev.w3.org/2006/xbl2/#simple-shadow-example
[2] http://whatwg.org/C#the-details-element-0
[3] http://images.whatwg.org/sample-details-2.png
[4] http://lachy.id.au/dev/2011/details.html

-- 
Lachlan Hunt - Opera Software
http://lachy.id.au/
http://www.opera.com/

Received on Tuesday, 5 April 2011 17:56:09 UTC