Skip to content

Commit

Permalink
feat(breadcrumb): implement component
Browse files Browse the repository at this point in the history
  • Loading branch information
dpellier committed Jul 29, 2024
1 parent 85c9aae commit 6bda3ed
Show file tree
Hide file tree
Showing 41 changed files with 1,319 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/ods/react/tests/_app/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const componentNames = [
'medium',
'input',
'textarea',
'breadcrumb',
//--generator-anchor--
];

Expand Down
14 changes: 14 additions & 0 deletions packages/ods/react/tests/_app/src/components/ods-breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react-dom/client';
import { OdsBreadcrumb, OdsBreadcrumbItem } from 'ods-components-react';

const Breadcrumb = () => {
return (
<OdsBreadcrumb>
<OdsBreadcrumbItem href="" icon="home" />
<OdsBreadcrumbItem href="" label="Link" />
<OdsBreadcrumbItem href="" label="Text" />
</OdsBreadcrumb>
);
};

export default Breadcrumb;
23 changes: 23 additions & 0 deletions packages/ods/react/tests/e2e/ods-breadcrumb.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Page } from 'puppeteer';
import { goToComponentPage, setupBrowser } from '../setup';

describe('ods-breadcrumb react', () => {
const setup = setupBrowser();
let page: Page;

beforeAll(async () => {
page = setup().page;
});

beforeEach(async () => {
await goToComponentPage(page, 'ods-breadcrumb');
});

it('render the component correctly', async () => {
const elem = await page.$('ods-breadcrumb');
const boundingBox = await elem?.boundingBox();

expect(boundingBox?.height).toBeGreaterThan(0);
expect(boundingBox?.width).toBeGreaterThan(0);
});
});
5 changes: 3 additions & 2 deletions packages/ods/scripts/generate-typedoc-md.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

/**
* Script to generate the file spec.md for one specific component
* The file is created in <path>/documentation/specifications
* The script need a json typedoc file in <path>/docs-api/typedoc.json
* The file is created in <path>/documentation
* The script need a json typedoc file in <path>/typedoc.json
*
* You can pass an optional --prefix <value> to manage components that are not
* in the default "components" package, ex:
Expand Down Expand Up @@ -92,6 +92,7 @@ function getClasses(jsonItems) {

const props = children
.filter(({ kind, decorators }) => kind === ReflectionKind.Property && decorators[0].escapedText === 'Prop')
.filter((prop) => prop.flags.isPublic)
.map((prop) => {
return [
`### ${prop.name}`,
Expand Down
5 changes: 5 additions & 0 deletions packages/ods/src/components/breadcrumb/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Local Stencil command generates external ods component build at the root of the project
# Excluding them is a temporary solution to avoid pushing generated files
# But the issue may cause main build (ods-component package) to fails, as it detects multiples occurences
# of the same component and thus you have to delete all those generated dir manually
*/src/
19 changes: 19 additions & 0 deletions packages/ods/src/components/breadcrumb/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@ovhcloud/ods-component-breadcrumb",
"version": "17.1.0",
"private": true,
"description": "ODS Breadcrumb component",
"main": "dist/index.cjs.js",
"collection": "dist/collection/collection-manifest.json",
"scripts": {
"clean": "rimraf .stencil coverage dist docs-api www",
"doc": "typedoc --pretty --plugin ../../../scripts/typedoc-plugin-decorator.js && node ../../../scripts/generate-typedoc-md.js",
"lint:scss": "stylelint 'src/components/**/*.scss'",
"lint:ts": "eslint '{src,tests}/**/*.{js,ts,tsx}'",
"start": "stencil build --dev --watch --serve",
"test:e2e": "stencil test --e2e --config stencil.config.ts",
"test:e2e:ci": "tsc --noEmit && stencil test --e2e --ci --config stencil.config.ts",
"test:spec": "stencil test --spec --config stencil.config.ts --coverage",
"test:spec:ci": "tsc --noEmit && stencil test --config stencil.config.ts --spec --ci --coverage"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@use '../../../../../style/link';

:host(.ods-breadcrumb-item) {
display: inline-flex;
align-items: center;
font-weight: link.$ods-link-font-weight;
}

:host(.ods-breadcrumb-item--collapsed) {
display: none;
}

.ods-breadcrumb-item {
&__last {
color: var(--ods-color-text);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Component, Event, type EventEmitter, type FunctionalComponent, Host, Prop, h } from '@stencil/core';
import { type OdsIconName } from '../../../../icon/src/constants/icon-name';

@Component({
shadow: true,
styleUrl: 'ods-breadcrumb-item.scss',
tag: 'ods-breadcrumb-item',
})
export class OdsBreadcrumbItem {
@Prop() isCollapsed: boolean = false;
@Prop() isExpandable: boolean = false;
@Prop() isLast: boolean = false;

@Prop({ reflect: true }) public href!: string;
@Prop({ reflect: true }) public icon?: OdsIconName;
@Prop({ reflect: true }) public isDisabled: boolean = false;
@Prop({ reflect: true }) public label?: string;
@Prop({ reflect: true }) public referrerpolicy?: ReferrerPolicy;
@Prop({ reflect: true }) public rel?: HTMLAnchorElement['rel'];
@Prop({ reflect: true }) public target?: HTMLAnchorElement['target'];

@Event() odsBreadcrumbItemClick!: EventEmitter<globalThis.Event>;
@Event() odsBreadcrumbItemExpand!: EventEmitter<void>;

onExpandClick(e: Event): void {
e.preventDefault();
e.stopPropagation();

this.odsBreadcrumbItemExpand.emit();
}

onLinkClick(e: globalThis.Event): void {
if (!this.isDisabled) {
this.odsBreadcrumbItemClick.emit(e);
}
}

render(): FunctionalComponent {
return (
<Host class={{
'ods-breadcrumb-item': true,
['ods-breadcrumb-item--collapsed']: this.isCollapsed && !this.isExpandable,
}}>
{
this.isExpandable &&
<ods-link
href=""
label="&hellip;"
onClick={ (e: globalThis.Event) => this.onExpandClick(e) }>
</ods-link>
}

{
!this.isExpandable && (this.isLast
? <span class="ods-breadcrumb-item__last">
{ this.label }
</span>
: <ods-link
exportparts="link"
href={ this.href }
icon={ this.icon }
isDisabled={ this.isDisabled }
label={ this.label }
onClick={ (e: globalThis.Event) => this.onLinkClick(e) }
referrerpolicy={ this.referrerpolicy }
rel={ this.rel }
target={ this.target }>
</ods-link>
)
}
</Host>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$breadcrumb-spacing: 8px;

:host(.ods-breadcrumb) {
display: flex;
flex-wrap: wrap;
column-gap: $breadcrumb-spacing;
align-items: center;

::slotted(ods-breadcrumb-item:not(:last-child)) {
&::after {
padding-left: $breadcrumb-spacing;
color: var(--ods-color-text);
font-size: 1.25rem;
font-weight: 400;
content: '|';
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Component, Element, type FunctionalComponent, Host, Listen, h } from '@stencil/core';
import { expandItems, setupItems } from '../../controller/ods-breadcrumb';

@Component({
shadow: true,
styleUrl: 'ods-breadcrumb.scss',
tag: 'ods-breadcrumb',
})
export class OdsBreadcrumb {
@Element() el!: HTMLElement;

@Listen('odsBreadcrumbItemExpand')
onOdsBreadcrumbItemExpand(): void {
expandItems(Array.from(this.el.children));
}

onSlotChange(): void {
setupItems(Array.from(this.el.children));
}

render(): FunctionalComponent {
return (
<Host
class="ods-breadcrumb"
role="navigation">
<slot onSlotchange={ () => this.onSlotChange() }></slot>
</Host>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const MAX_ITEM = 4;

function expandItems(items: Element[]): void {
items
.filter((child) => child.tagName.toLowerCase() === 'ods-breadcrumb-item')
.forEach((item) => {
item.setAttribute('is-collapsed', 'false');
item.setAttribute('is-expandable', 'false');
});
}

function setupItems(items: Element[]): void {
const breadcrumbItems = items
.filter((child) => child.tagName.toLowerCase() === 'ods-breadcrumb-item');

if (breadcrumbItems.length) {
breadcrumbItems[breadcrumbItems.length - 1].setAttribute('is-last', 'true');
}

if (breadcrumbItems.length > MAX_ITEM) {
breadcrumbItems.forEach((breadcrumbItem, idx) => {
if (idx > 0 && idx < breadcrumbItems.length - 1) {
breadcrumbItem.setAttribute('is-collapsed', 'true');
}

if (idx === 1) {
breadcrumbItem.setAttribute('is-expandable', 'true');
}
});
}
}

export {
expandItems,
setupItems,
};
9 changes: 9 additions & 0 deletions packages/ods/src/components/breadcrumb/src/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Import here all the external ODS component that you need to run the current component
* when running dev server (yarn start) or e2e tests
*
* ex:
* import '../../text/src';
*/
import '../../icon/src';
import '../../link/src';
82 changes: 82 additions & 0 deletions packages/ods/src/components/breadcrumb/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html dir='ltr' lang='en'>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0' />
<title>Dev ods-breadcrumb</title>

<script type='module' src='/build/ods-breadcrumb.esm.js'></script>
<script nomodule src='/build/ods-breadcrumb.js'></script>
<link rel="stylesheet" href="/build/ods-breadcrumb.css">
</head>

<body>
<p>Default</p>
<ods-breadcrumb>
<ods-breadcrumb-item href="#"
icon="home">
</ods-breadcrumb-item>

<ods-breadcrumb-item href="#"
label="Link">
</ods-breadcrumb-item>

<ods-breadcrumb-item href="#"
icon="arrow-up"
label="Link icon">
</ods-breadcrumb-item>

<ods-breadcrumb-item href="#"
label="Text">
</ods-breadcrumb-item>
</ods-breadcrumb>

<p>Collapsed</p>
<ods-breadcrumb>
<ods-breadcrumb-item href="#"
icon="home">
</ods-breadcrumb-item>

<ods-breadcrumb-item href="#"
label="First">
</ods-breadcrumb-item>

<ods-breadcrumb-item href="#"
label="Second">
</ods-breadcrumb-item>

<ods-breadcrumb-item href="#"
label="Third">
</ods-breadcrumb-item>

<ods-breadcrumb-item href="#"
label="Last">
</ods-breadcrumb-item>
</ods-breadcrumb>

<p>Custom link style</p>
<ods-breadcrumb>
<ods-breadcrumb-item href="#"
icon="home">
</ods-breadcrumb-item>

<ods-breadcrumb-item href="#"
label="Link">
</ods-breadcrumb-item>

<ods-breadcrumb-item class="my-link"
href="#"
label="Link custom">
</ods-breadcrumb-item>

<ods-breadcrumb-item href="#"
label="Text">
</ods-breadcrumb-item>
</ods-breadcrumb>
<style>
.my-link::part(link) {
color: green;
}
</style>
</body>
</html>
3 changes: 3 additions & 0 deletions packages/ods/src/components/breadcrumb/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { OdsBreadcrumb } from './components/ods-breadcrumb/ods-breadcrumb';
export { OdsBreadcrumbItem } from './components/ods-breadcrumb-item/ods-breadcrumb-item';
export { type OdsBreadcrumbItemClickEvent, type OdsBreadcrumbItemExpandEvent } from './interfaces/events';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type OdsBreadcrumbItemClickEvent = CustomEvent<Event>;
type OdsBreadcrumbItemExpandEvent = CustomEvent<void>;

export {
type OdsBreadcrumbItemClickEvent,
type OdsBreadcrumbItemExpandEvent,
};
7 changes: 7 additions & 0 deletions packages/ods/src/components/breadcrumb/stencil.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getStencilConfig } from '../../config/stencil';

export const config = getStencilConfig({
args: process.argv.slice(2),
componentCorePackage: '@ovhcloud/ods-component-breadcrumb',
namespace: 'ods-breadcrumb',
});
Loading

0 comments on commit 6bda3ed

Please sign in to comment.