Wednesday, January 03, 2024

UI5 Tips: Manipulating the sap.m.TabContainer close buttons with custom CSS

Here's a ui5tip to show how you can change the look and feel of the sap.m.TabContainer with a minimal amount of custom CSS. If you want to try this for yourself, be sure to check out the sample application from github.

The sap.m.TabContainer

The sap.m.TabContainer provides a simple, no-nonsense widget to build a tabbed user interface (check out the samples). Tabs can be added via the items aggregation, which should contain a collection of sap.m.TabContainerItem's. While this control generally suits my needs, it has one feature I find problematic: each tabs always has a close button, which appears as a little 'X' icon in the right side of the tab. If the user clicks it, it will actually 'close' the tab, that is: the respective sap.m.TabContainerItem will be removed from the TabContainer. See the screenshot below to see what the default looks like (close buttons highligthed in red):default sap.m.TabContainer with close buttons on each tab.

Suppressing the close action

The openui5 samples show how you can suppress that behavior: you can write an event handler for the itemClose event, and then call the preventDefault() method on the event: (In the view xml:)
<m:TabContainer itemClose="onTabContainerItemClose">
  <m:items>
    <m:TabContainerItem>
       ...
    </m:TabContainerItem>
    <m:TabContainerItem>
       ...
    </m:TabContainerItem>
  </m:items>
</m:TabContainer>
(In the controller javascript:)
  onTabContainerItemClose: function(event){
    event.preventDefault();
  }
Obviously, it would be strange if we'd always prevent the tab from being closed: Suppressing the default action of closing the tab only makes sense in a context where the user is supposed to be able to close the tab at all, and in such a case this could be used to pop up a dialog to let the user choose if they really meant to close the tab or want to keep it open. But the use case I frequently encounter is that the tab should not be closeable in the first place. While suppressing the close action would ensure the tab is never closed, it would confuse and anger the user, as the close button itself would still be there, inviting users to perform an action that can never be fulfilled.

Using the other Tab widget

One might suggest to use the sap.m.IconTabBar widget instead of the sap.m.TabContainer. The sap.m.IconTabBar takes sap.m.IconTabFilter's in its items collection, and these do not have a close button. Now, in some cases the sap.m.IconTabBar/sap.m.IconTabFilter may suit your needs and then you're fine. However I find that it has a number of other drawbacks (which I won't get into here). Besides, the sap.m.IconTabBar introduces a similar problem, but the other way around: whereas we cannot get rid of the close button in the sap.m.TabContainer, we cannot ever have a close button in the sap.m.IconTabBar. What we really want, is a property or something like that, which will let us control whether the tab will have a close button or not.

CSS to the rescue

In the previous section we argued that we'd really like to be able to control for each individual tab whether they have a close action at all, for example, by setting a property. To add a property one would normally have to extend a ui5 control, and attach some code so that the property setting can somehow influence the behavior of the control - in this case, control whether or not the close button will be displayed. While this is probably possible (I haven't tried it for this case), it does seem like an extraordinary measure for such a humble request. I found that a similar effect can be achieved by applying some custom CSS in combination with standard ui5 features. That's what this entire sample is about. With this tip you can:
  • hide all close buttons for an entire sap.m.TabContainer
  • hide the close button on an individual sap.m.TabContainerItem
  • show the close button on an individual sap.m.TabContainerItem in case the close buttons are hidden by default on the sap.m.TabContainer
All this functionality requires the inclusion of some css. In the sample this is all isolated in a single ui5-customization.css file, which is included into the application by declaring it the manifest.json.

Hiding all close buttons

To hide all the close buttons for all sap.m.TabContainerItem in the items collection of a particular sap.m.TabContainer, simply add the noCloseButtons style class:
<m:TabContainer 
  class="noCloseButtons" 
>
  <m:items>
    <!-- note: close button will be hidden by default for each m:TabContainerItem -->
    ...
  </m:items>
</m:TabContainer>
This works because the class property in the ui5 xml view is rendered to the html dom directly. So whatever html elements that ui5 creates to implement the TabControl widget will then be selectable with a class selector in css, and this is how we can relatively simply influence the look of our TabContainer through css. In our ui5-customization.css file, this is how we use noCloseButtons class to hide the buttons:
div.sapMTabContainer.noCloseButtons > .sapMTabStripContainer > .sapMTabStrip > .sapMTSTabsContainer > .sapMTSTabs > .sapMTabStripItem > .sapMTSItemCloseBtnCnt {
  visibility: hidden;
}
(Note the initial selector, div.sapMTabContainer.noCloseButtons and the chain of > child selectors which target the actual bit of html that is used to render the close button, which is simply hidden by setting the css visibility property to hidden.) In the sample application, you can see this behavior in action in the App.view.xml. This contains the code for the outermost tabcontainer. A screenshot is shown below and as you can see both tabs ("Hide individually" and "Show individually") do not have a close button: Hiding the close buttons on a sap.m.TabContainer by applying the noCloseButtons css class.

Hiding an individual close button

If we can add a custom css class to sap.m.TabContainer to hide all close buttons, then surely it should be possible to follow the same approach for an individual sap.m.TabContainerItem, right? Yes, it should, but sadly, we cannot. (The reason is that sap.m.TabContainer is a subclass of sap.m.Control, which provides a addStyleClass() method, whereas sap.m.TabContainerItem is a subclass of sap.ui.core.Element, which does not have such a method) Now, let's take a step back and think about how we used the css style class on the sap.m.TabContainer to hide all the close buttons. By setting the custom css style class on the sap.m.TabContainer, the html dom was changed to include the custom class, and we could then use that in a css selector. So even if we cannot apply a css style class to a sap.m.TabContainerItem, might there be another way that would allow us to influence how ui5 writes the html dom so we may write a css selector in our custom css? It turns out that such a feature exists in the shape of a feature called ui5 custom data. The custom data aggregation is provided by sap.ui.core.Element and thus available to its subclasses, including sap.m.TabContainerItem. A custom data item is an arbitrary key/value pair, and by setting its writeToDom property, ui5 will render it to the html dom as a html data attribute. To see what it looks like in our sample, take a look at TabContainerItemWithHiddenCloseButton.fragent.xml, which uses it to hide the close button in the second sap.m.TabContainerItem, in an otherwise normal sap.m.TabContainer:
<m:TabContainerItem
  id="item2"
  name="No Close Button"
>
  <m:customData>
    <core:CustomData writeToDom="true" key="noCloseButton" value="true"/>
  </m:customData>
  ...
</m:TabContainerItem>
Because the CusomtData's writeToDom property is set to true, ui5 will generate a html data attribute to the html dom that looks something like this:
data-noclosebutton='true'
And in our ui5-customization.css file, the following rule is intended to pick that up and hide the close button:
div.sapMTabContainer > .sapMTabStripContainer > .sapMTabStrip > .sapMTSTabsContainer > .sapMTSTabs > .sapMTabStripItem[data-noclosebutton='true'] > .sapMTSItemCloseBtnCnt {
  visibility: hidden;
}
As you can see it is very similar to the rule we used to hide all close buttons on any sap.m.TabContainer having the noCloseButtons class, except now the class is missing and instead we use a css predicate selector based on the data attribute:
.sapMTabStripItem[data-noclosebutton='true']
The screenshot below shows what it looks like in the app. Note that the tab named "Default" has the close button as usual, but the one named "No Close Button - the one with the custom data attribute - does not show a close button: Individual TabContainerItem with a hidden close button.

Showing an individual close button

The final hack in this sample combines the style class and the custom data attribute. CSS allows us to write a selector that takes both the presence of the css style class as well as the presence of a html data attribute into account. We can put this to good use if we want to have a sap.m.TabContainer that hides all close buttons by default, but undo the hiding of the close button of specific sap.m.TabContainerItem's, based on the value of a data attribute (which is in turn controlled by the ui5 Custom Data feature). You an see this in action in the TabContainerItemWithHiddenCloseButtons.fragment.xml file of the example:
<m:TabContainer 
  class="noCloseButtons"
>
  <m:items>

    <m:TabContainerItem name="Default">
     ...
    </m:TabContainerItem>

    <m:TabContainerItem name="Show Close Button">
      <m:customData>
        <core:CustomData key="noCloseButton" value="false" writeToDom="true" />
      </m:customData>
      ..
    </m:TabContainerItem>

  </m:items>
</m:TabContainer>
Again the second sap.m.TabContainerItem has a CustomData item with the key noCloseButton, but now the value is true so as to override the effect of the noCloseButtons style class applied to the sap.m.TabContainer. In our our ui5-customization.css file, the following rule is intended to pick that up and show the close button:
div.sapMTabContainer > .sapMTabStripContainer > .sapMTabStrip > .sapMTSTabsContainer > .sapMTSTabs > .sapMTabStripItem[data-noclosebutton='false'] > .sapMTSItemCloseBtnCnt {
  visibility: visible;
}
The screenshot below shows what it looks like when you run the sample application: Show the close button in an individual TabContainerItem, overriding the effect of the noCloseButtons style class of the sap.m.TabContainer.

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...