Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: rewrite breadcrumbs to use Lit and add tests #32

Merged
merged 14 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ dist/

# os
.DS_Store

.nyc_output
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [0.1.2](https://github.com/fabric-ds/elements/compare/v0.1.1...v0.1.2) (2022-04-25)


### Bug Fixes

* decouple elements and toast api to fix SSR usage ([f7e57cb](https://github.com/fabric-ds/elements/commit/f7e57cb4139a2942c6d971ba650b30a2c825d27d))

## [0.1.2-next.1](https://github.com/fabric-ds/elements/compare/v0.1.1...v0.1.2-next.1) (2022-04-25)


Expand Down
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@fabric-ds/elements",
"type": "module",
"version": "0.1.2-next.1",
"version": "0.1.2",
"description": "Custom elements for Fabric",
"exports": {
".": "./dist/index.js",
Expand Down Expand Up @@ -30,7 +30,7 @@
"test:mock-backend:ci": "node ./tests/utils/broadcast-backend.js &",
"test:run-once": "web-test-runner tests/**/*.test.js --node-resolve --static-logging",
"test:run-watch": "web-test-runner tests/**/*.test.js --node-resolve --watch",
"test": "npm run test:run-once",
"test": "npm run test:run-once && tap ./packages/**/test.js --no-check-coverage",
"watch:test": "npm run test:run-watch",
"semantic-release": "semantic-release"
},
Expand All @@ -50,15 +50,17 @@
"@web/test-runner": "^0.13.27",
"autoprefixer": "^10.2.3",
"babel-eslint": "^10.1.0",
"eslint": "^7.18.0",
"fastify": "^3.27.3",
"lit-element": "^2.4.0",
"lit-html": "^1.3.0",
"playwright": "^1.19.2",
"cors": "^2.8.5",
"cz-conventional-changelog": "^3.3.0",
"element-collapse": "^0.9.1",
"element-collapse": "1.0.1",
"esbuild": "^0.14.0",
"eslint": "^7.18.0",
"express": "^4.17.3",
"lerna": "^3.22.1",
"lit-element": "^2.4.0",
"lit-html": "^1.3.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.2.4",
"postcss-cli": "^8.3.1",
Expand All @@ -70,7 +72,7 @@
"semantic-release": "^19.0.2",
"semantic-release-slack-bot": "^3.5.2",
"tailwindcss": "^2.0.2",
"tap": "^15.1.6",
"tap": "^16.0.0",
"typescript": "^4.3.5",
"vite": "^2.0.0-beta.56",
"vite-plugin-html": "^2.0.0-beta.6"
Expand Down
35 changes: 21 additions & 14 deletions packages/breadcrumbs/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { FabricWebComponent } from '../utils';
import { LitElement, html } from 'lit';

class FabricBreadcrumbs extends FabricWebComponent {
connectedCallback() {
const children = Array.from(this.children)
.map((child) => child.outerHTML)
.join('<span class="select-none" aria-hidden="true">/</span>');
const separator = html`<span class="select-none" aria-hidden="true">/</span>`;
const interleave = (arr) =>
[].concat(...arr.map((el) => [el, separator])).slice(0, -1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats the purpose of the [].concat here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map creates pairs of elements and separators [el, separator] [el, separator] [el, separator], concat joins these array pairs together so you end up with 1 array with [el, separator, el, separator, el, separator]. Then the slice trims off the last separator so you end up with [el, separator, el, separator, el]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't have to support Edge 16-18 or iOS11 we could instead array.flatMap(el => [el, separator]).slice(1)


this.shadowRoot.innerHTML += `<nav
aria-label="Her er du"
class="flex space-x-8"
>
<h2 class="sr-only">Her er du</h2>
${children}
</nav>`;
class FabricBreadcrumbs extends LitElement {
connectedCallback() {
super.connectedCallback();
// Grab existing children at the point that the component is added to the page
// Interleave "/" separator with breadcrumbs
this._children = interleave(Array.from(this.children));
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intercept the child elements when the component is first added to the page and interleave them with slash character separators. Store the result as the internal property _children for use in the render method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interleaving is reactive in Vue (and I think React), but not here. Are we OK with that? Should that be disclosed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure. It is perhaps indicative of one of the differences between react/vue and custom elements. I toyed around with using dedicated child elements instead of this which is maybe one solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with us making a call that people won't treat the children as reactive and fixing it later.

I think this is a lot easier to get into trouble with in Vue/React than it is manually monkeying with DOM elements. But I defer to your judgement here!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you say not reactive do you mean in the context of using this custom element in React or Vue, or as in if the user updates the props, it wouldn't update the DOM element? @pearofducks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean this implementation in the custom element isn't watching children - so if the user adds/removes breadcrumbs it won't re-interleave them.


this.innerHTML = '';
render() {
return html`
<style>
@import "https://assets.finn.no/pkg/@fabric-ds/css/v1/fabric.min.css";
</style>
<nav aria-label="Her er du" class="flex space-x-8">
<h2 class="sr-only">Her er du</h2>
${this._children}
</nav>
`;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The render method ignores the original children and instead renders out the interleaved ones stored in this._children

}
}

Expand Down
114 changes: 114 additions & 0 deletions packages/breadcrumbs/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* eslint-disable no-undef */
import tap, { test, beforeEach, teardown } from 'tap';
import { chromium } from 'playwright';

tap.before(async () => {
const browser = await chromium.launch({ headless: true });
tap.context.browser = browser;
});

beforeEach(async (t) => {
const { browser } = t.context;
const context = await browser.newContext();
// context.setDefaultTimeout(2000);
t.context.page = await context.newPage();
});

teardown(async () => {
const { browser } = tap.context;
browser.close();
});

test('Breadcrumb component renders on the page', async (t) => {
// GIVEN: A component with 1 breadcrumb
const component = `
<f-breadcrumbs>
<a href="#/url/1">Eiendom</a>
</f-breadcrumbs>
`;

// WHEN: the component is added to the page
const { page } = t.context;
await page.setContent(component);
await page.addScriptTag({ path: './dist/index.js', type: 'module' });

// THEN: the component is visible in the DOM
t.equal(await page.innerText('nav a'), 'Eiendom', 'An Eiendom a tag should be added to the page');
});

test('Breadcrumb component interleaves / characters between breadcrumb items', async (t) => {
// GIVEN: A component with 2 breadcrumbs
const component = `
<f-breadcrumbs>
<a href="#/url/1">Eiendom</a>
<a href="#/url/2">Torget</a>
</f-breadcrumbs>
`;

// WHEN: the component is added to the page AND spans are selected
const { page } = t.context;
await page.setContent(component);
await page.addScriptTag({ path: './dist/index.js', type: 'module' });

// THEN: a single divider should have been interleaved with the breadcrumbs
t.equal(await page.innerText('f-breadcrumbs span'), '/', 'Divider slashes should be added');
});

test('Breadcrumb component with anchor child elements', async (t) => {
// GIVEN: A component with 3 breadcrumbs
const component = `
<f-breadcrumbs>
<a href="#/url/1">Eiendom</a>
<a href="#/url/2">Torget</a>
<a href="#/url/3">Oslo</a>
</f-breadcrumbs>
`;

// WHEN: the component is added to the page AND a elements are selected
const { page } = t.context;
await page.setContent(component);
await page.addScriptTag({ path: './dist/index.js', type: 'module' });

// THEN: there should be three breadcrumbs in the DOM
t.equal(await page.locator('f-breadcrumbs a').count(), 3, '3 a tags should be present');
t.equal(await page.locator('f-breadcrumbs span').count(), 2, '2 span tags should be present');
t.match(
await page.innerText(':nth-match(f-breadcrumbs a, 1)'),
'Eiendom',
'The first segment should be Eiendom',
);
t.match(
await page.innerText(':nth-match(f-breadcrumbs a, 2)'),
'Torget',
'The second segment should be Torget',
);
t.match(
await page.innerText(':nth-match(f-breadcrumbs a, 3)'),
'Oslo',
'The third segment should be Oslo',
);
});

test('Breadcrumb component with last element as a span', async (t) => {
// GIVEN: A component with 3 breadcrumbs
const component = `
<f-breadcrumbs>
<a href="#/url/1">Eiendom</a>
<a href="#/url/2">Torget</a>
<span aria-current="page">Oslo</span>
</f-breadcrumbs>
`;

// WHEN: the component is added to the page AND a elements are selected
const { page } = t.context;
await page.setContent(component);
await page.addScriptTag({ path: './dist/index.js', type: 'module' });

// THEN: there should be three breadcrumbs in the DOM
t.equal(await page.locator('f-breadcrumbs a').count(), 2, '2 child a tags should be present');
t.equal(
await page.locator('f-breadcrumbs span').count(),
3,
'3 child span tags should be present',
);
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests look how you would expect but are run (by web test runner) in a browser context so that document is available and so on.

25 changes: 22 additions & 3 deletions pages/components/breadcrumbs.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ <h1 class="mb-16">Breadcrumbs</h1>
</p>

<h2 class="mt-24 mb-16">Import</h2>
<syntax-highlight>npm install @fabric-ds/elements</syntax-highlight>
<syntax-highlight>import "@fabric-ds/elements";</syntax-highlight>

<h2 class="mt-24 mb-16">Examples</h2>

<h3>All segments linkable</h3>

<syntax-highlight>
<f-breadcrumbs class="mt-10">
<f-breadcrumbs>
<a href="#/url/1">Eiendom</a>
<a href="#/url/2">Bolig til salgs</a>
<a href="#/url/3" aria-current="page"> Oslo </a>
Expand All @@ -25,7 +27,24 @@ <h2 class="mt-24 mb-16">Examples</h2>
<f-breadcrumbs class="mt-10">
<a href="#/url/1">Eiendom</a>
<a href="#/url/2">Bolig til salgs</a>
<a href="#/url/3" aria-current="page"> Oslo </a>
<a href="#/url/3" aria-current="page">Oslo</a>
</f-breadcrumbs>
</div>

<h3>Disabled last link</h3>

<syntax-highlight>
<f-breadcrumbs>
<a href="#/url/1">Eiendom</a>
<a href="#/url/2">Bolig til salgs</a>
<span aria-current="page">Oslo</span>
</f-breadcrumbs></syntax-highlight
>
<div class="example">
<f-breadcrumbs class="mt-10">
<a href="#/url/1">Eiendom</a>
<a href="#/url/2">Bolig til salgs</a>
<span aria-current="page">Oslo</span>
</f-breadcrumbs>
</div>
</main>
Expand Down
16 changes: 0 additions & 16 deletions postcss.config.cjs

This file was deleted.