- Contributing
- Branches
- Developing
- Creating New Components
- Dev Sandbox
- Testing
- Documentation
- OnliveUI-flavoured Markdown
- Best Practices
- Accessibility
- Code Formatting
- Composability
- Component Structure
- Class Names
- Boolean Props
- Conditional Slots
- Dynamic Slot Names and Expand/Collapse Icons
- Fallback Content in Slots
- Custom Events
- Change Events
- CSS Custom Properties
- Focusing on Disabled Items
- When to use a property vs. a CSS custom property
- When to use a CSS custom property vs. a CSS part
- Naming CSS Parts
- Dependencies
- Form Controls
- System Icons
- Writing tests
Contributing
OnliveUI project meaning Onlive dev-team can use it and contribute to its development.
Branches
-
main
- This branch reflects the latest release and powers ui.onlive.site.
Developing
To set up a local dev environment, fork the repo from Gitlab, clone it locally, and install its dependencies.
git clone https://gitlab.com/onlive.site/onlive-ui/ cd onlive-ui pnpm install
Once you’ve cloned the repo, run the following command to spin up the dev server.
pnpm dev
After the initial build, a browser will open automatically to a local version of the docs. The documentation is powered by Eleventy and a number of custom plugins.
Creating New Components
To scaffold a new component, run the following command, replacing ol-tag-name
with the desired
tag name.
pnpm run create ol-tag-name
This will generate a source file, a stylesheet, and a docs page for you. When you start the dev server, you’ll find the new component in the “Components” section of the sidebar.
Dev Sandbox
Component development occurs within the local docs site. I’ve found that offering common variations in the docs is more beneficial for users than segmenting demos and code examples into separate tools such as Storybook. This encourages more thorough documentation, streamlines development for maintainers, and simplifies how the project is built. It also reduces installation and startup times significantly.
There is currently no hot module reloading (HMR), as browsers don’t provide a way to unregister custom elements. However, most changes to the source will reload the browser automatically.
For more information about running and building the project locally, refer to README.md
in the
project’s root.
Testing
OnliveUI uses Web Test Runner for testing. To launch the test runner during development, open a terminal and launch the dev server.
pnpm dev
In a second terminal window, launch the test runner.
pnpm test:watch
Follow the on-screen instructions to work with the test runner. Tests will automatically re-run as you make changes.
To run all tests only once:
pnpm test
To test a single component, use the component’s basename as shown in the following example.
pnpm test:component breadcrumb-item
Documentation
Maintaining good documentation can be a painstaking task, but poor documentation leads to frustration and makes the project less appealing to users. Fortunately, writing documentation for OnliveUI is fast and easy!
Most of OnliveUI’s technical documentation is generated with JSDoc comments and TypeScript metadata from the source code. Every property, method, event, etc. is documented this way. In-code comments encourage contributors to keep the documentation up to date as changes occur so the docs are less likely to become stale. Refer to an existing component to see how JSDoc comments are used in OnliveUI.
Instructions, code examples, and interactive demos are hand-curated to give users the best possible experience. Typically, the most relevant information is shown first and less common examples are shown towards the bottom. Edge cases and gotchas should be called out in context with tips or warnings.
The docs are powered by
Eleventy. Check out docs/components/*.md
to get an idea of how pages are structured and formatted. If
you’re creating a new component, it may help to use an existing component’s markdown file as a template.
OnliveUI-flavoured Markdown
The OnliveUI documentation uses an extended version of markdown-it. Generally speaking, it follows the Commonmark spec while sprinkling in some additional features.
Code Previews
To render a code preview, use the standard code field syntax and append :preview
to the
language.
```html:preview [code goes here] ```
You can also append :expanded
to expand the code by default, and :no-codepen
to
disable the CodePen button. The order of these modifiers doesn’t matter, but no spaces should exist between
the language and the modifiers.
```html:preview:expanded:no-codepen [code goes here] ```
This particular syntax was chosen for a few reasons:
- It’s easy to remember
- It works out of the box with markdown-it
- It appears to have the best support across editors and previewers (the language is usually highlighted correctly)
Callouts
Special callouts can be added using the following syntax.
:::tip This is a tip/informational callout ::: :::warning This is a warning callout ::: :::danger This is a danger callout :::
Asides
To place content that’s indirectly related, use the following syntax.
:::aside This content is indirectly related and will appear in an `<aside>` element. :::
Details
To provide additional details that can be expanded/collapses, use the following syntax.
:::details Title Here The details here are expandable. :::
Best Practices
The following is a non-exhaustive list of conventions, patterns, and best practices we try to follow. As a contributor, we ask that you make a good faith effort to follow them as well. This ensures consistency and maintainability throughout the project.
This section can be a lot to digest in one sitting, so don’t feel like you need to take it all in right now. Most contributors will be better off skimming this section and reviewing the relevant content as needed.
Accessibility
OnliveUI is built with accessibility in mind. Creating generic components that are fully accessible to users with varying capabilities across a multitude of circumstances is a daunting challenge. Oftentimes, the solution to an a11y problem is not written in black and white and, therefore, we may not get it right the first time around. There are, however, guidelines we can follow in our effort to make OnliveUI an accessible foundation from which applications and websites can be built.
It’s important to remember that, although accessibility starts with foundational components, it doesn’t end with them. It everyone’s responsibility to encourage best practices and ensure we’re providing an optimal experience for all of our users.
Code Formatting
Most code formatting is handled automatically by Prettier via commit hooks. However, for the best experience, you should install it in your editor and enable format on save.
Please do not make any changes to prettier.config.cjs
without consulting the maintainers.
Composability
Components should be composable, meaning you can easily reuse them with and within other components. This reduces the overall size of the library, expedites feature development, and maintains a consistent user experience.
Component Structure
All components have a host element, which is a reference to the <ol-*>
element itself.
Make sure to always set the host element’s display
property to the appropriate value depending
on your needs, as the default is inline
per the custom element spec.
:host { display: block; }
Aside from display
, avoid setting styles on the host element when possible. The reason for this
is that styles applied to the host element are not encapsulated. Instead, create a base element that wraps
the component’s internals and style that instead. This convention also makes it easier to use BEM in
components, as the base element can serve as the “block” entity.
When authoring components, please try to follow the same structure and conventions found in other components. Classes, for example, generally follow this structure:
- Static properties/methods
- Private/public properties (that are not reactive)
@query
decorators@state
decorators@property
decorators-
Lifecycle methods (
connectedCallback()
,disconnectedCallback()
,firstUpdated()
, etc.) - Private methods
@watch
decorators- Public methods
- The
render()
method
Please avoid using the public
keyword for class fields. It’s simply too verbose when combined
with decorators, property names, and arguments. However, please do add private
in
front of any property or method that is intended to be private.
This might seem like a lot, but it’s fairly intuitive once you start working with the library. However, don’t let this structure prevent you from submitting a PR. Code can change and nobody will chastise you for “getting it wrong.” At the same time, encouraging consistency helps keep the library maintainable and easy for others to understand. (A lint rule that helps with things like this would be a very welcome PR!)
Class Names
All components use a shadow DOM, so styles are completely encapsulated from the rest of the document. As a result, class names used inside a component won’t conflict with class names outside the component, so we’re free to name them anything we want.
Internally, each component uses the BEM methodology for class names. There is no technical requirement to do this — it’s purely the preference of the author to enforce consistency and clarity throughout components. As such, all contributions are expected to follow this pattern.
Boolean Props
Boolean props should always default to false
, otherwise there’s no way for the user to
unset them using only attributes. To keep the API as friendly and consistent as possible, use a property
such as noHeader
and a corresponding kebab-case attribute such as no-header
.
When naming boolean props that hide or disable things, prefix them with no-
, e.g.
no-spin-buttons
and avoid using other verbs such as hide-
and
disable-
for consistency.
Conditional Slots
When a component relies on the presence of slotted content to do something, don’t assume its initial state is permanent. Slotted content can be added or removed any time and components must be aware of this. A good practice to manage this is:
- Add
@slotchange={this.handleSlotChange}
to the slots you want to watch -
Add a
handleSlotChange
method and use thehasSlot
utility to update state variables for the the respective slot(s) -
Never conditionally render
<slot>
elements in a component — always usehidden
so the slot remains in the DOM and theslotchange
event can be captured
See the source of card, dialog, or drawer for examples.
Dynamic Slot Names and Expand/Collapse Icons
A pattern has been established in <ol-details>
and <ol-tree-item>
for
expand/collapse icons that animate on open/close. In short, create two slots called
expand-icon
and collapse-icon
and render them both in the DOM, using CSS to
show/hide only one based on the current open state. Avoid conditionally rendering them. Also avoid using
dynamic slot names, such as <slot name=${open ? 'open' : 'closed'}>
, because Firefox will
not animate them.
There should be a container element immediately surrounding both slots. The container should be animated
with CSS by default and it should have a part so the user can override the animation or disable it. Please
refer to the source and documentation for <ol-details>
and/or
<ol-tree-item>
for details.
Fallback Content in Slots
When providing fallback content inside of <slot>
elements, avoid adding parts, e.g.:
<slot name="icon"> <ol-icon part="close-icon"></ol-icon> </slot>
This creates confusion because the part will be documented, but it won’t work when the user slots in their own content. The recommended way to customize this example is for the user to slot in their own content and target its styles with CSS as needed.
Custom Events
Components must only emit custom events, and all custom events must start with ol-
as a
namespace. For compatibility with frameworks that utilize DOM templates, custom events must have lowercase,
kebab-style names. For example, use ol-change
instead of slChange
.
This convention avoids the problem of browsers lowercasing attributes, causing some frameworks to be unable to listen to them. This problem isn’t specific to one framework, but Vue’s documentation provides a good explanation of the problem.
Change Events
When change events are emitted by OnliveUI components, they should be named ol-change
and they
should only be emitted as a result of user input. Programmatic changes, such as setting
el.value = '…'
should not result in a change event being emitted. This is consistent
with how native form controls work.
CSS Custom Properties
To expose custom properties as part of a component’s API, scope them to the :host
block.
:host { --color: var(--ol-color-primary-500); --background-color: var(--ol-color-neutral-100); }
Then use the following syntax for comments so they appear in the generated docs. Do not use the
--ol-
prefix, as that is reserved for design tokens that live in the global scope.
/** * @cssproperty --color: The component's text color. * @cssproperty --background-color: The component's background color. */ export default class OlExample { // ... }
Focusing on Disabled Items
When an item within a keyboard navigable set is disabled (e.g. tabs, trees, menu items, etc.), the disabled item should not receive focus via keyboard, click, or tap. It should be skipped just like in operating system menus and in native HTML form controls. There is no exception to this. If a particular item requires focus for assistive devices to provide a good user experience, the item should not be disabled and, upon activation, it should inform the user why the respective action cannot be completed.
When to use a property vs. a CSS custom property
When designing a component’s API, standard properties are generally used to change the behavior of a component, whereas CSS custom properties (“CSS variables”) are used to change the appearance of a component. Remember that properties can’t respond to media queries, but CSS variables can.
There are some exceptions to this (e.g. when it significantly improves developer experience), but a good rule of thumbs is “will this need to change based on screen size?” If so, you probably want to use a CSS variable.
When to use a CSS custom property vs. a CSS part
There are two ways to enable customizations for components. One way is with CSS custom properties (“CSS variables”), the other is with CSS parts (“parts”).
CSS variables are scoped to the host element and can be reused throughout the component. A good example of a
CSS variable would be --border-width
, which might get reused throughout a component to ensure
borders share the same width for all internal elements.
Parts let you target a specific element inside the component’s shadow DOM but, by design, you can’t target a part’s children or siblings. You can only customize the part itself. Use a part when you need to allow a single element inside the component to accept styles.
This convention can be relaxed when the developer experience is greatly improved by not following these suggestions.
Naming CSS Parts
While CSS parts can be named virtually anything, within OnliveUI they must use the kebab-case convention and lowercase letters. Additionally, a BEM-inspired naming convention is used to distinguish parts, subparts, and states.
When composing elements, use part
to export the host element and exportparts
to
export its parts.
render() { return html` <div part="base"> <ol-icon part="icon" exportparts="base:icon__base" ...></ol-icon> </div> `; }
This results in a consistent, easy to understand structure for parts. In this example, the
icon
part will target the host element and the icon__base
part will target the
icon’s base
part.
Dependencies
TL;DR – a component is a dependency if and only if it’s rendered inside another component’s shadow root.
Many OnliveUI components use other OnliveUI components internally. For example,
<ol-button>
uses both <ol-icon>
and
<ol-spinner>
for its caret icon and loading state, respectively. Since these components
appear in the button’s shadow root, they are considered dependencies of Button. Since dependencies are
automatically loaded, users only need to import the button and everything will work as expected.
Contrast this to <ol-select>
and <ol-option>
. At first, one might
assume that Option is a dependency of Select. After all, you can’t really use Select without slotting in at
least one Option. However, Option is not a dependency of Select! The reason is because no Option is
rendered in the Select’s shadow root. Since the options are provided by the user, it’s up to them to import
both components independently.
People often suggest that OnliveUI should auto-load Select + Option, Menu + Menu Item, Breadcrumb + Breadcrumb Item, etc. Although some components are designed to work together, they’re technically not dependencies so eagerly loading them may not be desirable. What if someone wants to roll their own component with a superset of features? They wouldn’t be able to if OnliveUI automatically imported it!
Similarly, in the case of <ol-radio-group>
there was originally only
<ol-radio>
, but now you can use either <ol-radio>
or
<ol-radio-button>
as child elements. Which component(s) should be auto-loaded
dependencies in this case? Had Radio been a dependency of Radio Group, users that only wanted Radio Buttons
would be forced to register both with no way to opt out and no way to provide their own customized version.
For non-dependencies, the user should decide what gets registered, even if it comes with a minor inconvenience.
Form Controls
Form controls should support submission and validation through the following conventions:
-
All form controls must use
name
,value
, anddisabled
properties in the same manner asHTMLInputElement
-
All form controls must have a
setCustomValidity()
method so the user can set a custom validation message -
All form controls must have a
reportValidity()
method that report their validity during form submission - All form controls must have an
invalid
property that reflects their validity -
All form controls should mirror their native validation attributes such as
required
,pattern
,minlength
,maxlength
, etc. when possible - All form controls must be tested to work with the standard
<form>
element
System Icons
Avoid inlining SVG icons inside of templates. If a component requires an icon, make sure
<ol-icon>
is a dependency of the component and use the
system library:
<ol-icon library="system" name="..."></ol-icon>
This will render the icons instantly whereas the default library will fetch them from a remote source. If an
icon isn’t available in the system library, you will need to add it to library.system.ts
. Using
the system library ensures that all icons load instantly and are customizable by users who wish to provide a
custom resolver for the system library.
Writing tests
What to test for a given component:
- Start with a simple test that checks that the default version of the component still renders.
- Add at least one accessibility test (The accessibility check only covers the parts of the DOM which are currently visible and rendered. Depending on the component, more than one accessibility test is required to cover all scenarios.):
const myComponent = await fixture<OlAlert>(html`<ol-my-component>SomeContent</ol-my-component>`); await expect(myComponent).to.be.accessible();
- Try to cover all features advertised in the component’s description
Guidelines for writing tests:
- Each test should declare its own, hand crafted hml fixture for the component. Do not try to write one big component to match all tests. This helps keeping each test understandable in isolation.
- Tests should not produce log lines. Note that sometimes this cannot be prevented as the test runner might log errors (e.g. 404s).
- Try keeping the main test readable: Extract more complicated sets of selectors/commands/assertions into separate functions.
- Try to aim testing the user facing features of the component instead of the internal workings of the component.
- Group multiple tests for one feature into describe blocks.