feat(helpers): new package with several helpers

This commit is contained in:
Joren Broekema 2020-01-13 10:42:13 +01:00 committed by Thomas Allmer
parent 614be5fb89
commit d13672a226
13 changed files with 657 additions and 0 deletions

View file

@ -0,0 +1,30 @@
# Helpers
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
A helpers package that contains several helpers that are used inside lion but can be used outside as well.
These helpers are considered developer tools, not actual things to use in production.
Therefore, they may not have the same quality standards as our other packages.
## Live Demo/Documentation
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/helpers) for a live demo and API documentation
## Installation
```bash
npm i @lion/helpers
```
## Usage
Example using the sb-action-logger helper component.
```html
<script type="module">
import '@lion/helpers/sb-action-logger.js';
</script>
<sb-action-logger></sb-action-logger>
```

View file

@ -0,0 +1,6 @@
// Utilities
export { renderLitAsNode } from './renderLitAsNode/src/renderLitAsNode.js';
// Components
export { SbActionLogger } from './sb-action-logger/src/SbActionLogger.js';
export { SbLocaleSwitcher } from './sb-locale-switcher/src/SbLocaleSwitcher.js';

View file

@ -0,0 +1,38 @@
{
"name": "@lion/helpers",
"version": "0.1.0",
"description": "Helpers that are used throughout lion and can be used outside",
"author": "ing-bank",
"homepage": "https://github.com/ing-bank/lion/",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/ing-bank/lion.git",
"directory": "packages/helpers"
},
"scripts": {
"prepublishOnly": "../../scripts/npm-prepublish.js"
},
"keywords": [
"lion",
"web-components",
"helpers",
"action logger"
],
"main": "index.js",
"module": "index.js",
"files": [
"sb-action-logger",
"*.js"
],
"dependencies": {
"@lion/core": "0.3.0"
},
"devDependencies": {
"@open-wc/demoing-storybook": "^1.6.1",
"@open-wc/testing": "^2.3.4"
}
}

View file

@ -0,0 +1,7 @@
import { render } from '@lion/core';
export const renderLitAsNode = litHtmlTemplate => {
const offlineRenderContainer = document.createElement('div');
render(litHtmlTemplate, offlineRenderContainer);
return offlineRenderContainer.firstElementChild;
};

View file

@ -0,0 +1,39 @@
import { expect } from '@open-wc/testing';
import { html } from '@lion/core';
import { renderLitAsNode } from '../src/renderLitAsNode.js';
describe('renderLitAsNode', () => {
it('should return a matching HTMLElement (Node)', () => {
const el = renderLitAsNode(html`
<div>
<a href="#" target="_blank">Link</a>
Some text: <span>Hello, World</span>
</div>
`);
expect(el).to.be.instanceOf(HTMLElement);
expect(el).dom.to.equal(`
<div>
<a href="#" target="_blank">Link</a>
Some text: <span>Hello, World</span>
</div>
`);
});
it('should only render and return the first (root) node in the template', () => {
const el = renderLitAsNode(html`
<div>
<a href="#" target="_blank">Link</a>
Some text: <span>Hello, World</span>
</div>
<div>Sibling div</div>
`);
expect(el).dom.to.equal(`
<div>
<a href="#" target="_blank">Link</a>
Some text: <span>Hello, World</span>
</div>
`);
});
});

View file

@ -0,0 +1,3 @@
import { SbActionLogger } from './sb-action-logger/src/SbActionLogger.js';
window.customElements.define('sb-action-logger', SbActionLogger);

View file

@ -0,0 +1,35 @@
# Storybook Action Logger
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
A visual element to show action logs in Storybook demos `sb-action-logger`.
**This is a demonstrative tool**, not a debugging tool (although it may help initially).
If you try logging complex values such as arrays, objects or promises,
you should expect to get only the string interpretation as the output in this logger.
## Live Demo/Documentation
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/helpers-storybook-action-logger) for a live demo and API documentation
## Installation
```bash
npm i @lion/helpers
```
## Usage
```html
<script type="module">
import '@lion/helpers/sb-action-logger/sb-action-logger.js';
</script>
<sb-action-logger></sb-action-logger>
```
Then, with the sb-action-logger instance selected, call the `log` method on it.
```js
myActionLoggerInstance.log('Hello, World!');
```

View file

@ -0,0 +1,46 @@
{
"version": 0,
"tags": [
{
"name": "sb-action-logger",
"description": "A visual element to show action logs in Storybook demos",
"properties": [
{
"name": "title",
"type": "String",
"description": "The title of action logger",
"default": "Action Logger"
}
],
"events": [],
"slots": [],
"cssProperties": [
{
"name": "--sb-action-logger-title-color",
"description": "Color of the title",
"type": "Color"
},
{
"name": "--sb-action-logger-text-color",
"description": "Color of the logs' text",
"type": "Color"
},
{
"name": "--sb-action-logger-cue-color-primary",
"description": "Primary color of the visual cue",
"type": "Color"
},
{
"name": "--sb-action-logger-cue-color-secondary",
"description": "Secondary color of the visual cue",
"type": "Color"
},
{
"name": "--sb-action-logger-cue-duration",
"description": "Duration of the visual cue",
"type": "Number"
}
]
}
]
}

View file

@ -0,0 +1,178 @@
import { css, html, LitElement, render } from '@lion/core';
/** @typedef {import('lit-html').TemplateResult} TemplateResult */
export class SbActionLogger extends LitElement {
static get properties() {
return {
title: { type: String, reflect: true },
__logCounter: { type: Number },
};
}
static get styles() {
return css`
:host {
--sb-action-logger-title-color: black;
--sb-action-logger-text-color: black;
--sb-action-logger-cue-color-primary: #3f51b5;
--sb-action-logger-cue-color-secondary: #c5cae9;
--sb-action-logger-cue-duration: 1000ms;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
display: block;
}
.header__info {
color: var(--sb-action-logger-title-color);
display: flex;
align-items: center;
padding: 16px;
font-size: 16px;
}
.header__clear {
margin-left: 16px;
border-radius: 0px;
background-color: rgba(0, 0, 0, 0.05);
border: none;
cursor: pointer;
padding: 8px;
}
.header__clear:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.header__title {
margin: 0;
font-weight: bold;
flex-grow: 1;
}
.header__log-cue {
position: relative;
height: 3px;
background-color: var(--sb-action-logger-cue-color-secondary);
overflow: hidden;
}
.header__log-cue-overlay {
position: absolute;
height: 3px;
width: 50px;
left: -50px;
background-color: var(--sb-action-logger-cue-color-primary);
}
.header__log-cue-overlay--slide {
animation: slidethrough var(--sb-action-logger-cue-duration) ease-in;
}
@keyframes slidethrough {
from {
left: -50px;
width: 50px;
}
to {
left: 100%;
width: 500px;
}
}
.logger {
overflow-y: auto;
max-height: 110px;
}
.logger__log {
padding: 16px;
}
.logger__log:not(:last-child) {
border-bottom: 1px solid lightgrey;
}
.logger__log code {
color: var(--sb-action-logger-text-color);
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
`;
}
constructor() {
super();
this.title = 'Action Logger';
this.__logCounter = 0;
}
/**
* Renders the passed content as a node, and appends it to the logger
* Only supports simple values, will be interpreted to a String
* E.g. an Object will become '[object Object]'
*
* @param {} content Content to be logged to the action logger
*/
log(content) {
const loggerEl = this.shadowRoot.querySelector('.logger');
const offlineRenderContainer = document.createElement('div');
render(this._logTemplate(content), offlineRenderContainer);
// TODO: Feature, combine duplicate consecutive logs as 1 dom element and add a counter for dupes
loggerEl.appendChild(offlineRenderContainer.firstElementChild);
this.__logCounter += 1;
this.__animateCue();
loggerEl.scrollTo({ top: loggerEl.scrollHeight, behavior: 'smooth' });
}
/**
* Protected getter that returns the template of a single log
*
* @return {TemplateResult} TemplateResult that uses the content passed to create a log
*/
// eslint-disable-next-line class-methods-use-this
_logTemplate(content) {
return html`
<div class="logger__log">
<code>${content}</code>
</div>
`;
}
__animateCue() {
const cueEl = this.shadowRoot.querySelector('.header__log-cue-overlay');
cueEl.classList.remove('header__log-cue-overlay--slide');
// This triggers browser to stop batching changes because it has to evaluate something.
// eslint-disable-next-line no-void
void this.offsetWidth;
// So that when we arrive here, the browser sees this adding as an actual 'change'
// and this means the animation gets refired.
cueEl.classList.add('header__log-cue-overlay--slide');
}
__clearLogs() {
const loggerEl = this.shadowRoot.querySelector('.logger');
loggerEl.innerHTML = '';
this.__logCounter = 0;
}
render() {
return html`
<div class="header">
<div class="header__info">
<p class="header__title">${this.title}</p>
<div class="header__counter">${this.__logCounter}</div>
<button class="header__clear" @click=${this.__clearLogs}>Clear</button>
</div>
<div class="header__log-cue">
<div class="header__log-cue-overlay"></div>
</div>
</div>
<div class="logger"></div>
`;
}
}

View file

@ -0,0 +1,116 @@
import {
Story,
Preview,
Meta,
Props,
html,
} from '@open-wc/demoing-storybook';
import '../../sb-action-logger.js';
<Meta
title="Helpers/Storybook Action Logger"
parameters={{
component: "sb-action-logger",
options: { selectedPanel: "storybookjs/knobs/panel" }
}}
/>
# Storybook Action Logger
A visual element to show action logs in Storybook demos `sb-action-logger`
<Story name="Default">
{html`
<style>
sb-action-logger {
font-family: 'Nunito Sans', -apple-system, '.SFNSText-Regular', 'San Francisco',
BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
</style>
<div>To log: <code>Hello, World!</code></div>
<button
@click=${() => document.getElementById('logger-a53209ghj').log('Hello, World!')}
>Click this button</button>
<sb-action-logger id="logger-a53209ghj"></sb-action-logger>
`}
</Story>
```html
<div>To log: <code>Hello, World!</code></div>
<button
@click=${() => document.getElementById('logger-a53209ghj').log('Hello, World!')}
>Click this button</button>
<sb-action-logger id="logger-a53209ghj"></sb-action-logger>
```
Note that you need some reference to your logger. Above example shows this by using a unique ID.
## Features:
- A public method `log` to log things to the action logger.
- Overridable `title` property.
- Clear button to clear logs
- A counter to count the total amount of logs
## How to use
### Installation
```bash
npm i sb-action-logger
```
## API
<Props of="sb-action-logger" />
## Variations
### Custom Title
<Story name="Custom Title">
{html`
<button
@click=${() => document.getElementById('logger-vnfoiu3478').log('Hello, World!')}
>Log</button>
<sb-action-logger id="logger-vnfoiu3478" .title=${'Hello World'}></sb-action-logger>
`}
</Story>
## Rationale
This component was created due to a need for a nice visual action logger in Storybook Docs mode.
In docs mode, you do not have access to the action logger addon component, and it is nice to be able to show actions inline in your demos and documentation.
### Opinionated
I added quite a bit of styling on this component to make it look decent.
There are a bunch of styles that you can easily override to make it fit your design system which I believe should be enough in most cases.
If you need more control you can always:
- Override the host styles, there's a few custom CSS props that you can override as well.
- Extend the component and apply your overrides
- Open an issue if you believe it would be good to make something more flexible / configurable
Maybe in the future I will abstract this component to a more generic (ugly) one with no styles, so it's more friendly as a shared component.
### Plugin
If you use an action logger inside your Story in Storybook, you will also see it in your canvas, and this may not be your intention.
One idea I have is that we can simplify the usage further by making this a Storybook (docs-)plugin or decorator or whatever.
I am not too familiar with them at the moment, but it would be cool if someone can simply enable an action logger option on a particular Story inside their .mdx,
and then actions are automatically logged to the visual logger below it. Would need to figure out how to catch the action and pass it to the visual logger element.
I have not investigated yet on the how, but that is the rough idea. Feel free to help out here :)
## Future
I plan on adding more features.
They can always be found in the test folder where I specify new features as tests first, and then I skip them until I implement them. Easy to find them that way.
If the feature you'd like is not in the tests, I probably did not think about it yet or did not plan to do it yet, so in that case feel free to make an issue so we can add it.
I'm happy to accept pull requests for skipped tests (features to be added), see the CONTRIBUTING.md on GitHub for more details on how to contribute to this codebase.

View file

@ -0,0 +1,125 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../sb-action-logger.js';
// Note: skips are left out of first iteration
describe('sb-action-logger', () => {
it('has a default title "Action Logger"', async () => {
const el = await fixture(html`
<sb-action-logger></sb-action-logger>
`);
expect(el.shadowRoot.querySelector('.header__title').innerText).to.equal('Action Logger');
});
it('has a title property / attribute that can be overridden', async () => {
const el = await fixture(html`
<sb-action-logger title="Logging your favorite fruit"></sb-action-logger>
`);
const titleEl = el.shadowRoot.querySelector('.header__title');
expect(titleEl.innerText).to.equal('Logging your favorite fruit');
});
describe('Logger behavior', () => {
it('is possible to send something to the logger using the log method', async () => {
const el = await fixture(html`
<sb-action-logger></sb-action-logger>
`);
el.log('Hello, World!');
const loggerEl = el.shadowRoot.querySelector('.logger');
expect(loggerEl.children.length).to.equal(1);
expect(loggerEl.firstElementChild.innerText).to.equal('Hello, World!');
});
it('appends new logs to the logger', async () => {
const el = await fixture(html`
<sb-action-logger></sb-action-logger>
`);
el.log('Hello, World!');
el.log('Hello, Planet!');
el.log('Hello, Earth!');
el.log('Hello, World!');
el.log('Hello, Planet!');
const loggerEl = el.shadowRoot.querySelector('.logger');
expect(loggerEl.children.length).to.equal(5);
});
it('shows a visual cue whenever something is logged to the logger', async () => {
const el = await fixture(html`
<sb-action-logger></sb-action-logger>
`);
const cueEl = el.shadowRoot.querySelector('.header__log-cue-overlay');
expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.false;
el.log('Hello, World!');
expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.true;
});
it('has a visual counter that counts the amount of total logs', async () => {
const el = await fixture(html`
<sb-action-logger></sb-action-logger>
`);
const cueEl = el.shadowRoot.querySelector('.header__log-cue-overlay');
expect(cueEl.classList.contains('.header__log-cue-overlay--slide')).to.be.false;
el.log('Hello, World!');
expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.true;
});
it('has a clear button that clears the logs and resets the counter', async () => {
const el = await fixture(html`
<sb-action-logger></sb-action-logger>
`);
el.log('Hello, World!');
el.log('Hello, Planet!');
const clearBtn = el.shadowRoot.querySelector('.header__clear');
clearBtn.click();
expect(el.shadowRoot.querySelector('.logger').children.length).to.equal(0);
});
it('duplicate consecutive logs are kept as one', async () => {
const el = await fixture(html`
<sb-action-logger></sb-action-logger>
`);
expect(el).to.be.true;
});
});
describe('Potential Additional Features', () => {
it.skip('duplicate consecutive adds a visual counter to count per duplicate', async () => {
const el = await fixture(html`
<sb-action-logger></sb-action-logger>
`);
expect(el).to.be.true;
});
// This is handy if you don't want to keep track of updates
it.skip('can be set to mode=simple for only showing a single log statement', async () => {
const el = await fixture(html`
<sb-action-logger simple></sb-action-logger>
`);
expect(el).to.be.true;
});
it.skip('fires a sb-action-logged event when something is logged to the logger', async () => {
const el = await fixture(html`
<sb-action-logger></sb-action-logger>
`);
expect(el).to.be.true;
});
});
});

View file

@ -0,0 +1,3 @@
import { SbLocaleSwitcher } from './sb-locale-switcher/src/SbLocaleSwitcher.js';
window.customElements.define('sb-locale-switcher', SbLocaleSwitcher);

View file

@ -0,0 +1,31 @@
import { LitElement, html } from '@lion/core';
export class SbLocaleSwitcher extends LitElement {
static get properties() {
return {
showLocales: { type: Array, attribute: 'show-locales' },
};
}
constructor() {
super();
this.showLocales = ['en-GB', 'en-US', 'en-AU', 'nl-NL', 'nl-BE'];
}
// eslint-disable-next-line class-methods-use-this
callback(locale) {
document.documentElement.lang = locale;
}
render() {
return html`
${this.showLocales.map(
showLocale => html`
<button @click=${() => this.callback(showLocale)}>
${showLocale}
</button>
`,
)}
`;
}
}