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

feat: add contained-list #2464

Merged
merged 30 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6de0411
feat: add contained-list
maicongodinho Feb 6, 2023
5116dcc
refactor: change ul for div in role list
maicongodinho Mar 3, 2023
c051bde
refactor: code style
maicongodinho Mar 3, 2023
ddb27f5
refactor: reduce story examples
maicongodinho Mar 3, 2023
0cf1132
refactor: optimize icon registration
maicongodinho Mar 3, 2023
0716b8b
refactor: single handling enums
maicongodinho Mar 3, 2023
81182a2
refactor: remove redundancy on template
maicongodinho Mar 3, 2023
1a432fe
fix: change icon to templateRef
maicongodinho Mar 23, 2023
b67e626
test: add contained list tests
maicongodinho Mar 23, 2023
b14014a
refactor: remove unnecessary story
maicongodinho Mar 24, 2023
6fd3c11
refactor: code style
maicongodinho Mar 24, 2023
d5a016e
refactor: code style
maicongodinho Mar 24, 2023
a05a498
refactor: remove unnecessary icons and move module
maicongodinho Mar 24, 2023
f47e5ae
refactor: add cds contained list selector
maicongodinho Mar 24, 2023
95baf06
feat: add icon str ref to contained list item
maicongodinho Mar 24, 2023
d23580f
feat: add search to contained list story
maicongodinho Mar 28, 2023
eee426c
fix: storybook refs
maicongodinho Jul 24, 2023
37ba7b3
refactor: remove host style
maicongodinho Aug 29, 2023
97b86aa
refactor: remove host class
maicongodinho Aug 29, 2023
e16c7ee
refactor: remove host bindings
maicongodinho Aug 29, 2023
e19be58
feat: export contained-list
maicongodinho Aug 29, 2023
fab6060
fix: add missing ng-package
maicongodinho Aug 29, 2023
88d0360
feat: update carbon.yml
maicongodinho Aug 29, 2023
fac6932
Delete src/contained-list/package.json in favor of ng-package.json
Akshat55 Aug 29, 2023
b8eb444
Merge branch 'master' into contained-list
Akshat55 Aug 29, 2023
8be2123
Update src/contained-list/contained-list.component.spec.ts
Akshat55 Oct 18, 2023
b85339d
Update src/contained-list/contained-list.component.spec.ts
Akshat55 Oct 18, 2023
ad0a6d6
Merge branch 'master' into contained-list
Akshat55 Oct 18, 2023
d4f2edc
Update src/contained-list/contained-list.component.ts
Akshat55 Oct 18, 2023
61e6dc6
test: use lowercase letters for label
Akshat55 Oct 18, 2023
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
9 changes: 9 additions & 0 deletions src/carbon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ assets:
name: Storybook
action: link
url: https://angular.carbondesignsystem.com/?path=/story/components-combobox
contained-list:
status: stable
framework: angular
externalDocsUrl: https://carbondesignsystem.com/components/contained-list/usage/
demoLinks:
- type: storybook
name: Storybook
action: link
url: https://angular.carbondesignsystem.com/?path=/story/components-contained-list
content-switcher:
status: stable
framework: angular
Expand Down
99 changes: 99 additions & 0 deletions src/contained-list/contained-list-item.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
HostBinding,
Input,
Output,
TemplateRef
} from "@angular/core";

@Component({
selector: "cds-contained-list-item, ibm-contained-list-item",
template: `
<ng-container *ngIf="clickable">
<button
class="cds--contained-list-item__content"
type="button"
[disabled]="disabled"
(click)="onClick()">
<ng-content select="[ibmContainedListItemButton]"></ng-content>
</button>
</ng-container>
<ng-container *ngIf="!clickable">
<div class="cds--contained-list-item__content">
<div *ngIf="icon" class="cds--contained-list-item__icon">
<ng-container *ngIf="!isTemplate(icon)"><svg [ibmIcon]="icon" size="16"></svg></ng-container>
<ng-template *ngIf="isTemplate(icon)" [ngTemplateOutlet]="icon"></ng-template>
</div>
<ng-content></ng-content>
</div>
</ng-container>
<div class="cds--contained-list-item__action" *ngIf="action">
<ng-template [ngTemplateOutlet]="action"></ng-template>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContainedListItem {
/**
* A slot for a possible interactive element to render within the item.
*/
@Input() action: TemplateRef<any>;

/**
* Whether this item is disabled.
*/
@Input() disabled = false;

/**
* Whether this item is clickable.
*/
@Input() clickable: boolean;

/**
* Provide an optional icon to render in front of the item's content.
*
* Note that if you intend to use this as a string ref, it's important to remember
* to register the icon that you wish to add. In this case, it's also worth noting
* that only icons with a size of 16 are currently supported.
*/
@Input() icon: TemplateRef<any> | string;

/**
* Emits click event.
*/
@Output() click = new EventEmitter<void>();

/**
* Host binding item class.
*/
@HostBinding("class.cds--contained-list-item") itemClass = true;

/**
* Host binding item role attribute
*/
@HostBinding("attr.role") role = "listitem";

/**
* Host binding clickable item class.
*/
@HostBinding("class.cds--contained-list-item--clickable") get itemClickableClass() {
return this.clickable;
}

/**
* Host binding item with icon class.
*/
@HostBinding("class.cds--contained-list-item--with-icon") get itemWithIconClass() {
return !!this.icon;
}

public onClick() {
this.click.emit();
}

public isTemplate(value: string | TemplateRef<any>) {
return value instanceof TemplateRef;
}
}
167 changes: 167 additions & 0 deletions src/contained-list/contained-list.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { ButtonModule } from "../button";
import { IconModule, IconService } from "../icon";
import { ContainedListItem } from "./contained-list-item.component";
import Apple16 from "@carbon/icons/es/apple/16";
import Fish16 from "@carbon/icons/es/fish/16";
import { ContainedList } from "./contained-list.component";
import { ContainedListKind, ContainedListSize } from "./contained-list.enums";

@Component({
template: `
<ng-template #label>
<h1>My Contained List</h1>
</ng-template>

<ng-template #action>
<ibm-icon-button
type="button"
kind="primary"
align="left"
description="Add">
<svg class="cds--btn__icon" ibmIcon="add" size="16"></svg>
</ibm-icon-button>
</ng-template>

<ng-template #icon>
<svg ibmIcon="fish" size="16"></svg>
</ng-template>

<cds-contained-list [label]="label" [action]="action">
<cds-contained-list-item>List item</cds-contained-list-item>
<cds-contained-list-item [icon]="icon">List item with icon</cds-contained-list-item>
<cds-contained-list-item icon="apple">List item with string ref icon</cds-contained-list-item>
<cds-contained-list-item [action]="action">List item with action</cds-contained-list-item>
<cds-contained-list-item #clickableListItem [clickable]="true">
<ng-container ibmContainedListItemButton>Clickable list item</ng-container>
</cds-contained-list-item>
</cds-contained-list>
`
})
class WrapperComponent {
constructor(private iconService: IconService) {
this.iconService.registerAll([Apple16, Fish16]);
}
}

describe("ContainedList", () => {
let component: ContainedList;
let fixture: ComponentFixture<ContainedList>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ContainedList, ContainedListItem, WrapperComponent],
imports: [IconModule, ButtonModule]
}).compileComponents();

fixture = TestBed.createComponent(ContainedList);
component = fixture.componentInstance;
});

it("should set default inputs", () => {
fixture.detectChanges();
expect(component.action).toBeUndefined();
expect(component.isInset).toBeFalsy();
expect(component.kind).toBe(ContainedListKind.OnPage);
expect(component.label).toBeUndefined();
expect(component.size).toBe(ContainedListSize.Large);
});

it("should display the label when a string is provided", () => {
const label = "My Contained List";
component.label = label;
fixture.detectChanges();

const labelElement = fixture.nativeElement.querySelector(".cds--contained-list__label");
expect(labelElement.textContent.trim()).toEqual(label);
});

it("should have the correct isInset class", () => {
component.isInset = true;
fixture.detectChanges();

const listElement: HTMLElement = fixture.nativeElement.querySelector(".cds--contained-list");
expect(listElement).toHaveClass("cds--contained-list--inset-rulers");
});

it("should have the correct size class", () => {
component.size = ContainedListSize.Small;
fixture.detectChanges();

const listElement: HTMLElement = fixture.nativeElement.querySelector(".cds--contained-list");
expect(listElement).toHaveClass("cds--contained-list--sm");
});

it("should have the correct kind class", () => {
component.kind = ContainedListKind.Disclosed;
fixture.detectChanges();

const listElement: HTMLElement = fixture.nativeElement.querySelector(".cds--contained-list");
expect(listElement).toHaveClass("cds--contained-list--disclosed");
});

describe("TemplateRefs", () => {
it("should render the label if it is a TemplateRef", () => {
const wrapperFixture: ComponentFixture<WrapperComponent> = TestBed.createComponent(WrapperComponent);
wrapperFixture.detectChanges();

const labelRefElement = wrapperFixture.nativeElement.querySelector(".cds--contained-list .cds--contained-list__label h1");
expect(labelRefElement.textContent.trim()).toBe("My Contained List");
});

it("should render the action if it is a TemplateRef", () => {
const wrapperFixture: ComponentFixture<WrapperComponent> = TestBed.createComponent(WrapperComponent);
wrapperFixture.detectChanges();

const actionElementRef = wrapperFixture.nativeElement.querySelector(".cds--contained-list .cds--contained-list__action ibm-icon-button");
expect(actionElementRef).toBeTruthy();
});
});

describe("ContainedListItem", () => {
it("should render the content", () => {
const wrapperFixture: ComponentFixture<WrapperComponent> = TestBed.createComponent(WrapperComponent);
wrapperFixture.detectChanges();

const listItemElement = wrapperFixture.debugElement.query(By.css(".cds--contained-list-item:nth-child(1)"));
expect(listItemElement.nativeElement.textContent.trim()).toBe("List item");
});

it("should render the icon", () => {
const wrapperFixture: ComponentFixture<WrapperComponent> = TestBed.createComponent(WrapperComponent);
wrapperFixture.detectChanges();

const iconElement = wrapperFixture.debugElement.query(By.css(".cds--contained-list-item:nth-child(2) svg[ibmIcon='fish']"));
expect(iconElement).toBeTruthy();
});

it("should render the icon", () => {
const wrapperFixture: ComponentFixture<WrapperComponent> = TestBed.createComponent(WrapperComponent);
wrapperFixture.detectChanges();

const iconElement = wrapperFixture.debugElement.query(By.css(".cds--contained-list-item:nth-child(3) svg[ng-reflect-ibm-icon='apple']"));
expect(iconElement).toBeTruthy();
});

it("should render the action", () => {
const wrapperFixture: ComponentFixture<WrapperComponent> = TestBed.createComponent(WrapperComponent);
wrapperFixture.detectChanges();

const actionElement = wrapperFixture.debugElement.query(By.css(".cds--contained-list-item:nth-child(4) ibm-icon-button"));
expect(actionElement).toBeTruthy();
});

it("should render with the clickable state", () => {
const wrapperFixture: ComponentFixture<WrapperComponent> = TestBed.createComponent(WrapperComponent);
wrapperFixture.detectChanges();

const clickableListItemElement = wrapperFixture.debugElement.query(By.css(".cds--contained-list-item:nth-child(5)"));
expect(clickableListItemElement.nativeElement).toHaveClass("cds--contained-list-item--clickable");

const buttonElement = clickableListItemElement.nativeElement.querySelector("button");
expect(buttonElement.textContent.trim()).toBe("Clickable list item");
});
});
});
88 changes: 88 additions & 0 deletions src/contained-list/contained-list.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
ChangeDetectionStrategy,
Component,
HostBinding,
Input,
TemplateRef
} from "@angular/core";
import { ContainedListKind, ContainedListSize } from "./contained-list.enums";

@Component({
selector: "cds-contained-list, ibm-contained-list",
template: `
<div
class="cds--contained-list"
[ngClass]="{
'cds--contained-list--inset-rulers': isInset,
'cds--contained-list--on-page': kind === ContainedListKind.OnPage,
'cds--contained-list--disclosed': kind === ContainedListKind.Disclosed,
'cds--contained-list--sm': size === ContainedListSize.Small,
'cds--contained-list--md': size === ContainedListSize.Medium,
'cds--contained-list--lg': size === ContainedListSize.Large,
'cds--contained-list--xl': size === ContainedListSize.ExtraLarge
}">
<div class="cds--contained-list__header">
<div [id]="labelId" class="cds--contained-list__label">
<ng-container *ngIf="!isTemplate(label)">{{ label }}</ng-container>
<ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label"></ng-template>
</div>

<div class="cds--contained-list__action" *ngIf="action">
<ng-template [ngTemplateOutlet]="action"></ng-template>
</div>
</div>
<div role="list" [attr.aria-labelledby]="labelId">
<ng-content></ng-content>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContainedList {
/** Used to generate unique IDs */
private static count = 0;

/**
* A slot for a possible interactive element to render within the list header.
*/
@Input() action: TemplateRef<any>;

/**
* Specify whether the dividing lines in between list items should be inset.
*/
@Input() isInset = false;

/**
* The kind of ContainedList you want to display.
*/
@Input() kind: ContainedListKind = ContainedListKind.OnPage;

/**
* A label describing the contained list.
*/
@Input() label: string | TemplateRef<any>;

/**
* Specify the size of the contained list.
*/
@Input() size: ContainedListSize = ContainedListSize.Large;

/**
* Label id for the contained list.
*/
readonly labelId = `contained-list-${ContainedList.count++}-header`;

/**
* Exposing ContainedListSize enum to the template
*/
public ContainedListSize: typeof ContainedListSize = ContainedListSize;

/**
* Exposing ContainedListKind enum to the template
*/
public ContainedListKind: typeof ContainedListKind = ContainedListKind;

public isTemplate(value: string | TemplateRef<any>) {
return value instanceof TemplateRef;
}
}
11 changes: 11 additions & 0 deletions src/contained-list/contained-list.enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum ContainedListSize {
Small = "sm",
Medium = "md",
Large = "lg",
ExtraLarge = "xl"
}

export enum ContainedListKind {
OnPage = "on-page",
Disclosed = "disclosed"
}
12 changes: 12 additions & 0 deletions src/contained-list/contained-list.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ContainedList } from "./contained-list.component";
import { ContainedListItem } from "./contained-list-item.component";
import { IconModule } from "carbon-components-angular/icon";

@NgModule({
declarations: [ContainedList, ContainedListItem],
exports: [ContainedList, ContainedListItem],
imports: [CommonModule, IconModule]
})
export class ContainedListModule {}
Loading