Skip to content

Development Guidelines

Jenna Badanowski edited this page Sep 5, 2019 · 2 revisions

Code Styles

Coding standards

Every CSS rule should contain a single selector, with only a few exceptions

  • Layout and component styles should be a single class (specificity 0-1-0)
.foo { }

Never use an ID selector

  • IDs are difficult to override (specificity 1-0-0)
#bar { }

Attribute selectors can be used with element and class selectors for specific uses

  • Single form elements may be styled to indicate state or type based on attributes (specificity 0-1-1)
.fd-fieldset[disabled] { }
  • Single classes may be styled for to indicate accessibility state based on attributes (specificity 0-2-0)
.nav [aria-selected=true] { }
.navitem[aria-haspopup] { }
  • Include state classes along with ARIA attributes (specificity 0-2-0)
.nav [aria-selected=true], .nav .is-selected { }
.navitem[aria-haspopup], .nav .has-popup { }

Note: It is always preferable to use ARIA attributes directly for accessibility purposes. Talk to your JS/backend developers and establish this contract early in your project.

Component data structures

When developing a component, we must always consider that the data determines everything about what you see on the screen. There are flavors of data which can be characterized as 1) presentation, 2) content, and 3) interactivity or state.

To maintain consistency across all components, each has a constructor that accepts a standard data object separated by concerns.

A basic example ...

modifier: {
  block: []
},
properties: {},
state: {},
aria: {}

Let's look at each in more detail.

modifier

To separate presentation from content, the modifier object holds all the variants needed for a single component.

  • Use modifier.block to apply modifiers to the block/root, e.g., block: ["vertical"] would output the class tn-card--vertical for a card.
  • Use additional keys to apply modifiers to component elements, e.g., image: ["circle"] would output tn-card__image--circle.

The component templates can accept either a string or an array.

properties

This object is only about content data such as titles, descriptions, urls, etc. The properties of each component will be customized but the names should match element names in the HTML and CSS as closely as possible. For example, properties.title: "Card Title" should have a corresponding HTML representation such as <h1 class="tn-card__title">Card Title</h1>.

Always try to use a common vocabulary across components. Be descriptive of usage not visual representation, i.e., favor header, body, footer over top, middle, bottom.

Individual properties can be objects on their own when it makes sense. However, if the data object becomes complex, that's a good sign that sub-components may be needed.

"logo": {
  "url": "path/to/logo",
  "width": "200",
  "height": "90"
}

state

This is the interactivity part of the component. Options like disabled, readonly, status will be common. States are most often output as is- classes that can be toggled by page scripts based on certain behaviors, i.e., disabled: true results in <button class="tn-button is-disabled"></button> where the CSS selector .tn-button.is-disabled tightly binds the styles.

NOTE: These state classes are the primary contract between UI and back-end. Angular developers should not need to worry about modifier classes, they should learn to toggle is- classes and [aria-] attributes. Keeps things simple and clean.

aria

Very similar to state these indicate interactivity. In most cases, these mirror state classes, however, ARIA is preferred for accessibility advantages.

ARIA and states classes are both included for convenience. In the CSS, these are usually together such as .tn-button.is-disabled, .tn-button[aria-disabled=true] ensuring they do the same thing.

button.aria: { "label": "Submit" } outputs <button class="fd-button fd-button--light sap-icon--submit" aria-label="{{ aria.label }}"></button>

classes

Passing additional classes should also be considered when building a new component. Helper classes are a good example when a "one-off" presentation override is needed but not worth creating a new modifier, like adding a background color to an identifier. <div class="fd-identifier fd-has-background-color-accent-1">.

Nunjucks templates

Each Nunjucks macro should accept the API in a predictable but there are some variations on how they are authored. Generally, properties should be expanded to individual params and some common modifiers may be explicitly set as well.

Each macro file should define default values in the data object. This makes building example and test pages easier.

Let's look at some different types ...

Simple components

Some modules are very simple. Like token which has no modifiers or states. At the top of the token.njk file define the defaults. This will ensure that these values are used when using the component in other pages and components — {{ token() }}.

{% set defaults = {
    properties: {
      label: "Label"
    }
  }
%}

The token macro is very simple with only one properties param. However, since the token is used as a button we are setting the role as the default.

{% macro token(
  label=defaults.properties.label,
  classes=""
  ) -%}
<span class="fd-tag{{ classes | classes }}" role="button">{{ label }}</span>
{%- endmacro %}
classes

The classes param should always be included on every component. This allows other classes to be passed in — classes="card__token" when necessary. The namespace gets applied by the Nunjucks filter.

tokens {{ token(label="Card Label", classes="card__token") }} //outputs <span class="fd-token fd-card__token" role="button">Card Label</span> {{ token(label="Card Label", classes=["has-background-color-accent-9", "has-color-text-5"]) }} //outputs <span class="fd-token fd-has-background-color-accent-9 fd-has-color-text-5" role="button">Card Label</span>

Presentation components

Some modules are simple in their properties but are heavily defined by their presentation, like buttons. There are many types of buttons available so it makes sense to expose some of the modifiers as params.

The goal here is to simplify using the component. So instead of passing in a full modifiers object, you can pass only the params needed. And since no other modifications are allowed, that object is not handled at all.

{% set defaults = {
    properties:{
      label: "Button",
      icon: "pool"
    }
  }
%}
{%- macro button(
    label=defaults.properties.label,
    icon="",
    size="",
    type="",
    color="",
    state={},
    aria={},
    classes=""
  ) -%}
<button class="fd-button{{ ' fd-button--'+size if size }}{{ ' fd-button--'+type if type }}{{ ' fd-button--'+color if color }}{{ ' sap-icon--'+icon if icon }}{{ state | state }}{{ classes | classes }}"{{ aria | aria }}>{%- if label %}{{label}}{%- endif %}</button>
{%- endmacro %}

As an added convenience, a second icon_button macro can be added to handle the icon-only button.

This passes thorough most params to the button but it merges the label as a value into the aria object. See all custom filters in app.js

{%- macro icon_button(
    label=defaults.properties.label,
    icon=defaults.properties.icon,
    size="",
    type="",
    color="",
    state={},
    aria={},
    classes=""
  ) -%}
{%- set aria_obj = { label: label } | merge_objs(aria) %}
{{ button(
  label="",
  aria=aria_obj,
  icon=icon,
  size=size, type=type, color=color, state=state, classes=classes
) }}
{%- endmacro %}

Compound components

When a component has one or more children that may hve their own modifications and states, those should be dividing into separate macros. This is most useful for layout containers where large blocks of content are inside.

The button-group is a basic example.

button-group

{% set defaults = {
    properties: {
      label: "Sort by"
    }
  }
%}
{% macro button_group(label="", classes="") -%}
<div class="fd-button-group{{ classes | classes }}" role="group" aria-label="{{label}}">
  {{- caller() | indent -}}
</div>

Since the macro has a body, it is called differently.

{% call button_group(label="Sort by") -%}
  {{ icon_button(size="compact",label="Survey",icon="survey") }}
  {{ icon_button(size="compact",label="Chart",icon="pie-chart") }}
  {{ icon_button(size="compact",label="Pool",icon="pool") }}
{%- endcall %}