Wednesday, January 03, 2024

UI5 Tips: Change expand/collapse icons for Tree, Panel and TreeTable using only CSS

UI5 offers a couple of widgets that can expand and collapse. To do that, these controls render a button with an icon that indicates the current state, and which the user can click to toggle the state. The standard icons that UI5 renders for the expand/collapse button are navigation arrows, which some of our users disliked. In this tip, you'll learn how you can replace them with more appropriate icons using only a few lines of CSS. No javascript code is involved. If you want to check out this tip yourself, download the app from the expandcollapse directory and expose it to your webserver. You can then navigate to index.html to see the sample app in effect.

UI5 exandable/collapsible Controls

First, lets take a look at the standard UI5 controls.

Panel

The sap.m.Panel has an expandable property. If true the Panel renders a button which the user can use to hide and show the contents of the panel. A screenshot is shown below: An expandable sap.m.Panel (This screenshot is taken from UI5's Panel - Expand / Collapse sample)

Tree

The sap.m.Tree is a classical way of presenting hierarchically organized items like a folder structure. A screenshot is shown below: A sap.m.Tree control (This screenshot is taken from UI5's Tree - Basic sample)

TreeTable

The sap.ui.table.TreeTable is just like a regular data grid table (sap.ui.table.Table), but with an added functionality to hierarchically organize the rows in the table, and with the ability to expand or collapse rows according to the hierarchy. A screenshot is shown below: A sap.ui.table.TreeTable (This screenshot is taken from UI5's sap.ui.table.TreeTable JSONTreeBinding sample)

A look at the icons

Let's take a look at the standard icons that UI5 renders for the expand/collapse button: UI5 Right arrow icon UI5 Down arrow icon

Proposed Icons

While I don't really have a problem with these icons, some of our users had a problem recognizing the collapse/expand functionality for Panels. We looked a bit around in the UI5 Icon explorer and decided we'd rather use these icons instead: UI5 Expand icon UI5 Expand icon Going by their name, it's a bit of a mystery to me why they weren't used by UI5 in the first place. But anway, now we have this tip to explain how you can change them.

CSS to change the icons

We prepared a separate CSS file for each of the aforementioned UI5 controls, and included them into the app via the manifest.json:
"resources": {
  "css": [
    { "uri": "css/ui5-customization-m.Panel.css" },
    { "uri": "css/ui5-customization-m.TabContainer.css" },
    { "uri": "css/ui5-customization-m.Tree.css" },
    { "uri": "css/ui5-customization-ui.tree.TreeTable.css" }
  ]
}

How UI5 renders icons

Before we discuss how to apply the CSS to change the icons, it's useful to understand how UI5 icon rendering works. In general, UI5 uses icon fonts. The UI5 framework loads a library.css stylesheet, which has a @font-face rule like this:
@font-face {
    font-family: "SAP-icons";
    src: url('../base/fonts/SAP-icons.woff2') format('woff2'),
         url('../base/fonts/SAP-icons.woff') format('woff'),
         url('../base/fonts/SAP-icons.ttf') format('truetype'),
         local('SAP-icons');
    font-weight: normal;
    font-style: normal
}
This binds the name SAP-icons to the font resource, and will ensure that whenever a HTML element is assigned the font-family: "SAP-icons" css property, it will render whatever text it contains with glyphs from that font. Now, when using the UI5 javascript API, you don't actually ever have to deal with these details at this level. Rather, if you ever need to assign an icon explicitly, for example, when using a sap.ui.core.Icon control, you can assign a custom icon uri using the sap-icon protocol, which maps more or less reasonable icon names to the glyph that depicts the desired icon. (You can read more about the sap-icon uri protocol in the Icon topic of the SAP UI5 walkthrough) Apart from these explicitly assigned icons, the renderer classes of various UI5 controls will write out the required HTML code for the icons that just happen to be fixed to it. Let's call these structural icons. For example, there is no property that allows you to change the icon that a sap.m.Panel uses for its exapand/collapse button - that's just part of how the Panel happens to be coded - it's part of its structure. As we will see in the following sections, the font-family is just the underlying medium that allows the UI5 framework to render icons. The details of how a particular control renderer renders its structural icons can still vary a bit, and we'll need to figure out how a particular control renders its icons before we can change them.

How sap.ui.table.TreeTable renders the collapse/expand icons

The sap.ui.table.TreeTable renderer takes a straightforward approach to rendering the collapse/expand icons. If you open one of the standard UI5 TreeTable samples, and right click the expand/collapse icon to inspect it (for example, with the Chrome developer tools), then you might see something like this: Inspecting the sap.ui.table.TreeTable expand/collapse icon with chrome developer tools The sap.ui.table.TreeTable renderer has written a <span> element with a sapUiTableTreeIcon class:
<span 
  class="
    sapUiTableTreeIcon 
    sapUiTableTreeIconNodeClosed
" 
  title="Expand Node" 
  role="button" 
  aria-expanded="false"
></span>
The span does not actually contain any text - rather a css ::before pseudo class is used for that. This is also used to bind it to the "SAP-icons" font, using the font-family property - this ensures that element will render glyphs from the icon font:
.sapUiTableTreeIcon::before {
    font-family: "SAP-icons";
    font-size: .75rem;
    color: #0854a0;
}
The actual text content that determines the icon is controlled through another rule, using another css class, which uses the css content property to write out the character that renders the appropriate icon from the font. When collapsed, its:
.sapUiTableTreeIcon.sapUiTableTreeIconNodeClosed::before {
    content: '\e066';
}
(You may recall that \e066 is the character that corresponds to the navigation-right-arrow icon.) When expanded, its:
.sapUiTableTreeIcon.sapUiTableTreeIconNodeOpen::before {
    content: '\e1e2';
}
(You may recall that \e1e2 is the character that corresponds to the navigation-down-arrow icon.) This way, the sap.ui.TreeTable only needs to change the style class from sapUiTableTreeIconNodeClosed to sapUiTableTreeIconNodeOpen on the <span>, depending on the expanded/collapsted state of the row: the css magic will take care of rendering the right icon.

Changing the expand/collapse icons for the sap.ui.table.TreeTable

As we have just witnessed, the sap.ui.table.TreeTable uses separate classes for the collapse and expand icons. This makes it really quite simple to change the icons. We only have to write our own rules for the sapUiTableTreeIconNodeOpen::before and sapUiTableTreeIconNodeClosed::before classes to mask the default ones, and assign the proper value for the content property:
/**
* sap.ui.table.TreeTable: better icons for expanded
*/
.sapUiTableTreeIcon.sapUiTableTreeIconNodeOpen::before {
  content: '\e1d9';
}
/**
* sap.ui.table.TreeTable: better icons for collapsed
*/
.sapUiTableTreeIcon.sapUiTableTreeIconNodeClosed::before {
  content: '\e1da';
}
(You will find similar rules in the ui5-customization-ui.tree.TreeTable.css provided by this ui5tip) The only thing we really need to think of when applying this stylesheet is that it is loaded after UI5 framework loads the CSS specific to the ui.tree.TreeTable control: if our CSS is loaded before the framework's CSS, then our rules will be masked by the framework's, and we want to do it exactly the other way around. To ensure that the framework's CSS for the ui.tree.TreeTable control is loaded before our custom CSS, simply include the sap.ui.table library in the data-sap-ui-libs property of the <script> element you use to load UI5. (see the index.html for this tip):
<script
  id="sap-ui-bootstrap"
  src="https://openui5.hana.ondemand.com/1.87.0/resources/sap-ui-core.js"
  data-sap-ui-theme="sap_belize"
  data-sap-ui-libs="sap.m, sap.ui.table"
  data-sap-ui-bindingSyntax="complex"
  data-sap-ui-compatVersion="edge"
  data-sap-ui-preload="async"
  data-sap-ui-resourceroots='{
    "ui5tips": "./"
  }'
></script>
That's it! The screenshot below shows what the TreeTable looks like in this tip's sample app: sap.ui.table.TreeTable with modified collapse/expand icons

How sap.m.Panel renders the collaps/expand icons

Let's take a look at how the sap.m.Panel renders its collapse/expand icon. We can again open UI5's own sap.m.Panel sample and use our browser's develpoment tools to inspect the page's HTML code: Inspecting sap.m.Panel's standard expand/collapse icon. Just like the sap.ui.table.TreeTable we discussed in the previous section, the sap.m.Panel renders a <span> element for the icon, which is assigned a CSS class to mark it as the icon, and wich is bound to the icon font face:
<span 
  data-sap-ui-icon-content=""
  class="
      sapUiIcon 
      sapUiIconMirrorInRTL 
      sapMBtnCustomIcon 
      sapMBtnIcon 
      sapMBtnIconLeft" 
  style="font-family: 'SAP\2dicons';"
></span>
And, just like for the sap.ui.table.TreeTable, there is a CSS rule to select the ::before pseudo-class, which has the content property to insert the appropriate character that corresponds to the glyph.
.sapUiIcon::before {
    content: attr(data-sap-ui-icon-content);
    speak: none;
    font-weight: normal;
    -webkit-font-smoothing: antialiased;
}
There are some remarkable differences too with respect to the sap.ui.tree.TreeTable example. In this case, there are no separate classes corresponding to the collapsed/expanded state of the Panel. Instead, the content property of the .sapUiIcon::before pseudo-class uses the value of the elements data-sap-ui-icon-content attribute. It will render whatever text is in the elements data-sap-ui-icon-content attribute. If you check the code for the <span>, you'll note the data-sap-ui-icon-content has been assigned some text, which is rendered as a so-called .notdef glyph, both in the developer tools and here on the page. (The .notdef glyph is the "boxed question mark"). You can copy the text from the data-sap-ui-icon-content attribute in the browser tools and paste it in a hex editor, or in a javascript string to figure out what its character code is, for example:
// decimal: 57839
"".charCodeAt(0)

// hex: 0xE1EF
("".charCodeAt(0)).toString(16)
Inspecting the value of the data-sap-ui-icon-content attribute It turns out that this corresponds to UI5's slim-arrow-down icon, which has a similar appearance to the down-arrow icon. If you collapse the panel and inspect it again, you'll notice that the value of the data-sap-ui-icon-content is now 0xE1ED, which corresponds to UI5's slim-arrow-right icon.

Changing the expand/collapse icons for the sap.m.Panel

Now, it's clear that we cannot simply mask the existing classes in the same way we did in the sap.ui.tree.TreeTable case. The reason is that in this case the icon is driven directly by an attribute value, not by change of style class. Since the icon is so clearly driven by the value of the attribute, your initial hunch might be to somehow change the value that is written out to the HTML. But this would involve rewriting or overriding the sap.m.Panel or its renderer, and we're not quite prepared to do that just to change the icon. But, there is a way. What we can do is write some rules that match the <span> depending on the value of the data-sap-ui-icon-content attribute. And if we can match a CSS selector based on the attribute value, we can simply write out a content property with the desired character instead. This works as long as we know what values the attribute will have, which is of course the case here, as there will only be 2 different values, corresponding to the collapsed or expanded state of the panel. This is what it looks like in ui5-customization-m.Panel.css:
/*
  sap.m.Panel better expanded button. 

  The value in the predicate for data-sap-ui-icon-content may not render correctly,
  but this is decimal 57839, or 0xE1EF, which corresponds to UI5's "slim-arrow-down" icon
  (https://sapui5.hana.ondemand.com/sdk/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview/SAP-icons/?tab=grid&icon=slim-arrow-down)
  
*/
div.sapMPanel.sapMPanelExpandable > div > span[data-sap-ui-icon-content=].sapUiIcon::before {
  content: '\e1d9';
}

/*
  sap.m.Panel better collapse button
  
  The value in the predicate for data-sap-ui-icon-content may not render correctly,
  but this is decimal 57837, or 0xE1ED, which corresponds to UI5's "slim-arrow-right" icon
  (https://sapui5.hana.ondemand.com/sdk/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview/SAP-icons/?tab=grid&icon=slim-arrow-down)
*/
div.sapMPanel.sapMPanelExpandable > div > span[data-sap-ui-icon-content=].sapUiIcon::before {
  content: '\e1da';
}
Note the span[data-sap-ui-icon-content=].sapUiIcon::before is the essential bit that allows us to react to a specific icon value. The selector part before is there to ensure the rule will only apply to the expand/collapse button of a Panel, and not to some random other control's icon. And, here's what it looks like in the sample app: sap.m.Panel with modified expand/collapse icons.

How sap.m.Tree renders the collapse/expand icons

The sap.m.Tree uses exactly the same mechanism to render the icons as the sap.m.Panel does - the character that corresponds to the appropriate icon glyph is written to a data-sap-ui-icon-content, and the value of the attribute is rendered against the icon font's font face. The only difference with the Panel is that the sap.m.Tree uses the navigation-right-arrow and navigation-down-arrow icons, just like the sap.ui.table.TreeTable did. Apart from that, we also need to ensure the first bit of the selectors are specific to the sap.m.Tree, which is similar to what we did for the sap.m.Panel. This is what the CSS looks like in ui5-customization-m.Tree.css:
/**
  sap.m.TreeItem : better icons for collapsed 

  The value in the predicate for data-sap-ui-icon-content may not render correctly,
  but this is decimal 57446, or 0xE066, which corresponds to UI5's "navigation-right-arrow" icon
  (https://sapui5.hana.ondemand.com/sdk/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview/SAP-icons/?tab=grid&icon=navigation-right-arrow)

*/

li.sapMTreeItemBase > span[data-sap-ui-icon-content=].sapMTreeItemBaseExpander.sapUiIcon::before {
  content: '\e1da';
}


/**
  sap.m.TreeItem : better icons for expanded

  The value in the predicate for data-sap-ui-icon-content may not render correctly,
  but this is decimal 57826, or 0xE1E2, which corresponds to UI5's "navigation-down-arrow" icon
  (https://sapui5.hana.ondemand.com/sdk/test-resources/sap/m/demokit/iconExplorer/webapp/index.html#/overview/SAP-icons/?tab=grid&icon=navigation-down-arrow)
*/

li.sapMTreeItemBase > span[data-sap-ui-icon-content=].sapMTreeItemBaseExpander.sapUiIcon::before {
  content: '\e1d9';
}
And this is what the Tree looks like in the sample app: A sap.m.Tree with modified expand/collapse icons.

Finally

Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem. Want more tips? Find other posts with the ui5tips tag!

No comments:

DuckDB Bag of Tricks: Reading JSON, Data Type Detection, and Query Performance

DuckDB bag of tricks is the banner I use on this blog to post my tips and tricks about DuckDB . This post is about a particular challenge...