-
Notifications
You must be signed in to change notification settings - Fork 56
Development Guidelines
Every CSS rule should contain a single selector, with only a few exceptions
- Core styles should be a single element (specificity 0-0-1)
body { }
- 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)
fieldset[disabled] { }
input[type=password] { }
- 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.
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.
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 classtn-card--vertical
for a card. - Use additional keys to apply modifiers to component elements, e.g.,
image: ["circle"]
would outputtn-card__image--circle
.
The component templates can accept either a string
or an array
.
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"
}
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.
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>
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">
.
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 ...
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 %}
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.
{{ 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>
Some modules are simple in their properties but are heavily defined by their presentation, like button
s. 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 thelabel
as a value into thearia
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 %}
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.
{% 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 %}