Skip to content

Commit

Permalink
Feat/breadcrumb (#63)
Browse files Browse the repository at this point in the history
* feat(breadcrumb): add component
* refactor(breadcrumb): rename isCollapsedItem into isExpandableItem
* refactor(breadcrumb): use link instead of button for expandable item
  • Loading branch information
dpellier authored Jun 7, 2023
1 parent 16a9200 commit be51df5
Show file tree
Hide file tree
Showing 99 changed files with 1,987 additions and 4 deletions.
40 changes: 40 additions & 0 deletions packages/design/components/breadcrumb/design-breadcrumb.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Preview

<osds-breadcrumb items='[{"href":"#home","label":"Home"},{"href":"#services","label":"Services"},{"href":"#products","label":"Products"},{"href":"#web","label":"Web"}]'></osds-breadcrumb>

## Description

A list of links showing the current page location in the navigational hierarchy.

## Usage

It has several usage :

- Displaying sub-pages of a site structure
- Show a step progress of a process
- Simplify site structure navigation in a quicker way

## Anatomy

A Breadcrumb component has two main areas :

- A list of Links joined with a separator ("|" character) referring to the parent pages
- A non-clickable text showing the user current page

## Placement

A Breadcrumb is used at the top of a web page, preferably start-aligned.

Its width is automatic, relative to its content and is not adjustable.
Behavior

When the Breadcrumb has more than 4 Links visible, an ellipsis is displayed as a replacement for the middle links.

A click on the ellipsis will expanded all previously hidden links inline, the collapsed state can't be redone afterwards.

The Breadcrumb links are kept inline, even on mobile viewports.
Variants

## Accessibility

All Links are accessible through tabulation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { OdsComponentAttributes } from '../../ods-component-attributes';
import { OdsIconName } from '../../icon/ods-icon-size';

export interface OdsBreadcrumbItemAttributes extends OdsComponentAttributes {
/** contrasted or not: see component principles */
contrasted?: boolean,
/** Item link to redirect to */
href: string;
/** Icon to display */
icon?: OdsIconName;
/** @internal */
isCollapsed: boolean;
/** @internal */
isExpandableItem: boolean;
/** @internal */
isLast: boolean;
/** Text to display */
label?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { OdsBreadcrumbItem } from './ods-breadcrumb-item';
import { OdsComponentController } from '../../ods-component-controller';

/**
* common controller logic for component used by the different implementations.
* it contains all the glue between framework implementation and the third party service.
*/
export class OdsBreadcrumbItemController extends OdsComponentController<OdsBreadcrumbItem> {
constructor(component: OdsBreadcrumbItem) {
super(component);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { OdsBreadcrumbItemAttributes } from './ods-breadcrumb-item-attributes';

export const odsBreadcrumbItemDefaultAttributesDoc = {
contrasted: false,
isCollapsed: false,
isExpandableItem: false,
isLast: false,
} as const;

export const odsBreadcrumbItemDefaultAttributes = odsBreadcrumbItemDefaultAttributesDoc as OdsBreadcrumbItemAttributes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { OdsComponentEvents } from '../../ods-component-events';

export interface OdsBreadcrumbItemEvents extends OdsComponentEvents {
/**
* Event triggered on collapsed item click
*/
odsBreadcrumbItemCollapsedClick: void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { OdsComponentMethods } from '../../ods-component-methods';

export interface OdsBreadcrumbItemMethods extends OdsComponentMethods {
// Methods
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { OdsBreadcrumbItemAttributes } from './ods-breadcrumb-item-attributes';
import { OdsBreadcrumbItemController } from './ods-breadcrumb-item-controller';
import { OdsBreadcrumbItemEvents } from './ods-breadcrumb-item-events';
import { OdsBreadcrumbItemMethods } from './ods-breadcrumb-item-methods';
import { OdsComponent } from '../../ods-component';
import { OdsComponentGenericEvents } from '../../ods-component-generic-events';
import { OdsComponentGenericMethods } from '../../ods-component-generic-methods';

/**
* interface description of all implementation of `ods-breadcrumb-item`.
* each implementation must have defined events, methods, attributes
* and one controller for the common behavior logic
*/
export type OdsBreadcrumbItem<ComponentMethods extends OdsComponentGenericMethods<OdsBreadcrumbItemMethods> = OdsComponentGenericMethods<OdsBreadcrumbItemMethods>,
ComponentEvents extends OdsComponentGenericEvents<OdsBreadcrumbItemEvents> = OdsComponentGenericEvents<OdsBreadcrumbItemEvents>> =
OdsComponent<ComponentMethods, ComponentEvents, OdsBreadcrumbItemAttributes, OdsBreadcrumbItemController>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './ods-breadcrumb-item';
export * from './ods-breadcrumb-item-attributes';
export * from './ods-breadcrumb-item-controller';
export * from './ods-breadcrumb-item-default-attributes';
export * from './ods-breadcrumb-item-events';
export * from './ods-breadcrumb-item-methods';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { OdsComponentAttributes } from '../../ods-component-attributes';
import { OdsIconName } from '../../icon/ods-icon-size';

export interface OdsBreadcrumbAttributeItem {
/** Item link to redirect to */
href: string,
/** Icon to display */
icon?: OdsIconName,
/** Text to display */
label?: string,
}

export interface OdsBreadcrumbAttributes extends OdsComponentAttributes {
/** contrasted or not: see component principles */
contrasted?: boolean,
/**
* List of breadcrumb items to display
*/
items: OdsBreadcrumbAttributeItem[] | string,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface OdsBreadcrumbBehavior {
/**
* Reference to the host element.
*/
el: HTMLElement;

/**
* Items initialisation on first render
*/
componentWillLoad(): void;

/**
* Receive and handle a collapsed item click event.
*/
onBreadcrumbItemCollapsedClick(): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { OdsClearLoggerSpy, OdsInitializeLoggerSpy, OdsLoggerSpyReferences } from '@ovhcloud/ods-testing/src';
import { OdsLogger } from '../../../logger/ods-logger';
import { OdsBreadcrumb } from './ods-breadcrumb';
import { OdsBreadcrumbController } from './ods-breadcrumb-controller';
import { OdsBreadcrumbMock } from './ods-breadcrumb-mock';

describe('spec:ods-breadcrumb-controller', () => {
let controller: OdsBreadcrumbController;
let component: OdsBreadcrumb;
let loggerSpyReferences: OdsLoggerSpyReferences;

function setup(attributes: Partial<OdsBreadcrumb> = {}) {
component = { ...component, ...attributes };
controller = new OdsBreadcrumbController(component);
}

beforeEach(() => {
component = new OdsBreadcrumbMock();

const loggerMocked = new OdsLogger('myLoggerMocked');
loggerSpyReferences = OdsInitializeLoggerSpy({
loggerMocked: loggerMocked as never,
spiedClass: OdsBreadcrumbController,
});
});

afterEach(() => {
OdsClearLoggerSpy(loggerSpyReferences);
jest.clearAllMocks();
});

describe('methods', () => {
describe('getBreadcrumbItems', () => {
beforeEach(() => {
setup();
});

it('should return an empty array if there are no items', () => {
expect(controller.getBreadcrumbItems([], true)).toEqual([]);
});

it('should return all items non collapsed with last set', () => {
const dummyItems = [
{ href: 'dummy href 1'},
{ href: 'dummy href 2'},
];

expect(controller.getBreadcrumbItems(dummyItems, true)).toEqual([
{
...dummyItems[0],
isCollapsed: false,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[1],
isCollapsed: false,
isExpandableItem: false,
isLast: true,
},
]);
});

it('should return all middle items collapsed with last set', () => {
const dummyItems = [
{ href: 'dummy href 1'},
{ href: 'dummy href 2'},
{ href: 'dummy href 3'},
{ href: 'dummy href 4'},
{ href: 'dummy href 5'},
{ href: 'dummy href 6'},
];

expect(controller.getBreadcrumbItems(dummyItems, true)).toEqual([
{
...dummyItems[0],
isCollapsed: false,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[1],
isCollapsed: true,
isExpandableItem: true,
isLast: false,
},
{
...dummyItems[2],
isCollapsed: true,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[3],
isCollapsed: true,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[4],
isCollapsed: true,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[5],
isCollapsed: false,
isExpandableItem: false,
isLast: true,
},
]);
});

it('should return all items non collapsed if component is no more collapsed with last set', () => {
const dummyItems = [
{ href: 'dummy href 1'},
{ href: 'dummy href 2'},
{ href: 'dummy href 3'},
{ href: 'dummy href 4'},
{ href: 'dummy href 5'},
{ href: 'dummy href 6'},
];

expect(controller.getBreadcrumbItems(dummyItems, false)).toEqual([
{
...dummyItems[0],
isCollapsed: false,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[1],
isCollapsed: false,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[2],
isCollapsed: false,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[3],
isCollapsed: false,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[4],
isCollapsed: false,
isExpandableItem: false,
isLast: false,
},
{
...dummyItems[5],
isCollapsed: false,
isExpandableItem: false,
isLast: true,
},
]);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { OdsBreadcrumb } from './ods-breadcrumb';
import { OdsComponentController } from '../../ods-component-controller';
import { OdsBreadcrumbItemAttributes } from '../breadcrumb-item/ods-breadcrumb-item-attributes';
import { OdsBreadcrumbAttributeItem } from './ods-breadcrumb-attributes';

const MAX_DISPLAYED_ITEMS = 4;

/**
* common controller logic for component used by the different implementations.
* it contains all the glue between framework implementation and the third party service.
*/
export class OdsBreadcrumbController extends OdsComponentController<OdsBreadcrumb> {
constructor(component: OdsBreadcrumb) {
super(component);
}

getBreadcrumbItems(items: OdsBreadcrumbAttributeItem[], isCollapsed: boolean): OdsBreadcrumbItemAttributes[] {
if (!items.length) {
return [];
}

if (isCollapsed && items.length > MAX_DISPLAYED_ITEMS) {
return items.map((item, index) => ({
...item,
isCollapsed: index >= 1 && index < (items.length - 1),
isExpandableItem: index === 1,
isLast: index === (items.length - 1),
} as OdsBreadcrumbItemAttributes));
}

return items.map((item, index) => ({
...item,
isCollapsed: false,
isExpandableItem: false,
isLast: index === (items.length - 1),
} as OdsBreadcrumbItemAttributes));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { OdsBreadcrumbAttributes } from './ods-breadcrumb-attributes';

export const odsBreadcrumbDefaultAttributesDoc = {
contrasted: false,
} as const;

export const odsBreadcrumbDefaultAttributes = odsBreadcrumbDefaultAttributesDoc as OdsBreadcrumbAttributes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { OdsComponentEvents } from '../../ods-component-events';

export interface OdsBreadcrumbEvents extends OdsComponentEvents {
// Events
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { OdsComponentMethods } from '../../ods-component-methods';

export interface OdsBreadcrumbMethods extends OdsComponentMethods {
// Methods
}
Loading

0 comments on commit be51df5

Please sign in to comment.