diff --git a/skyux-spa-visual-tests/src/app/icon/icon-visual.component.html b/skyux-spa-visual-tests/src/app/icon/icon-visual.component.html
new file mode 100644
index 000000000..11edcef8d
--- /dev/null
+++ b/skyux-spa-visual-tests/src/app/icon/icon-visual.component.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/skyux-spa-visual-tests/src/app/icon/icon-visual.component.ts b/skyux-spa-visual-tests/src/app/icon/icon-visual.component.ts
new file mode 100644
index 000000000..86d657f81
--- /dev/null
+++ b/skyux-spa-visual-tests/src/app/icon/icon-visual.component.ts
@@ -0,0 +1,9 @@
+import {
+ Component
+} from '@angular/core';
+
+@Component({
+ selector: 'icon-visual',
+ templateUrl: './icon-visual.component.html'
+})
+export class IconVisualComponent { }
diff --git a/skyux-spa-visual-tests/src/app/icon/icon.visual-spec.ts b/skyux-spa-visual-tests/src/app/icon/icon.visual-spec.ts
new file mode 100644
index 000000000..526f2a412
--- /dev/null
+++ b/skyux-spa-visual-tests/src/app/icon/icon.visual-spec.ts
@@ -0,0 +1,17 @@
+import {
+ SkyVisualTest
+} from '../../../config/utils/visual-test-commands';
+
+describe('icon', () => {
+ it('should show the icon', () => {
+ return SkyVisualTest
+ .setupTest('icon')
+ .then(() => {
+ SkyVisualTest.moveCursorOffScreen();
+ return SkyVisualTest.compareScreenshot({
+ screenshotName: 'icon',
+ selector: '#screenshot-icon'
+ });
+ });
+ });
+});
diff --git a/skyux-spa-visual-tests/src/app/icon/index.html b/skyux-spa-visual-tests/src/app/icon/index.html
new file mode 100644
index 000000000..6c051cf61
--- /dev/null
+++ b/skyux-spa-visual-tests/src/app/icon/index.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/core.ts b/src/core.ts
index cbce611a5..d662e66a0 100644
--- a/src/core.ts
+++ b/src/core.ts
@@ -33,6 +33,7 @@ import { SkyFilterModule } from './modules/filter';
import { SkyFluidGridModule } from './modules/fluid-grid/fluid-grid.module';
import { SkyFlyoutModule } from './modules/flyout/flyout.module';
import { SkyGridModule } from './modules/grid';
+import { SkyIconModule } from './modules/icon';
import { SkyHelpInlineModule } from './modules/help-inline';
import { SkyInfiniteScrollModule } from './modules/infinite-scroll';
import { SkyKeyInfoModule } from './modules/key-info';
@@ -95,6 +96,7 @@ import { SkyWaitModule } from './modules/wait';
SkyFluidGridModule,
SkyFlyoutModule,
SkyGridModule,
+ SkyIconModule,
SkyHelpInlineModule,
SkyInfiniteScrollModule,
SkyKeyInfoModule,
@@ -160,6 +162,7 @@ export * from './modules/fluid-grid';
export * from './modules/flyout';
export * from './modules/format';
export * from './modules/grid';
+export * from './modules/icon';
export * from './modules/help-inline';
export * from './modules/infinite-scroll';
export * from './modules/key-info';
diff --git a/src/demo.ts b/src/demo.ts
index f4e2e0d45..c192beadf 100644
--- a/src/demo.ts
+++ b/src/demo.ts
@@ -38,6 +38,7 @@ import {
SkyFlyoutDemoComponent,
SkyGridDemoComponent,
SkyHelpInlineDemoComponent,
+ SkyIconDemoComponent,
SkyInfiniteScrollDemoComponent,
SkyKeyInfoDemoComponent,
SkyLabelDemoComponent,
@@ -119,6 +120,7 @@ const components = [
SkyFlyoutDemoComponent,
SkyFlyoutDemoInternalComponent,
SkyGridDemoComponent,
+ SkyIconDemoComponent,
SkyHelpInlineDemoComponent,
SkyInfiniteScrollDemoComponent,
SkyKeyInfoDemoComponent,
diff --git a/src/demos/demo.service.ts b/src/demos/demo.service.ts
index 8d529eb43..f0de32eeb 100644
--- a/src/demos/demo.service.ts
+++ b/src/demos/demo.service.ts
@@ -24,6 +24,7 @@ import {
SkyFlyoutDemoComponent,
SkyGridDemoComponent,
SkyHelpInlineDemoComponent,
+ SkyIconDemoComponent,
SkyInfiniteScrollDemoComponent,
SkyKeyInfoDemoComponent,
SkyLabelDemoComponent,
@@ -455,6 +456,22 @@ export class SkyDemoService {
}
]
},
+ {
+ name: 'Icon',
+ component: SkyIconDemoComponent,
+ files: [
+ {
+ name: 'icon-demo.component.html',
+ fileContents: require('!!raw-loader!./icon/icon-demo.component.html')
+ },
+ {
+ name: 'icon-demo.component.ts',
+ fileContents: require('!!raw-loader!./icon/icon-demo.component.ts'),
+ componentName: 'SkyIconDemoComponent',
+ bootstrapSelector: 'sky-icon-demo'
+ }
+ ]
+ },
{
name: 'Infinite scroll',
component: SkyInfiniteScrollDemoComponent,
diff --git a/src/demos/icon/icon-demo.component.html b/src/demos/icon/icon-demo.component.html
new file mode 100644
index 000000000..c01165ad8
--- /dev/null
+++ b/src/demos/icon/icon-demo.component.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/demos/icon/icon-demo.component.ts b/src/demos/icon/icon-demo.component.ts
new file mode 100644
index 000000000..ee0d628a0
--- /dev/null
+++ b/src/demos/icon/icon-demo.component.ts
@@ -0,0 +1,9 @@
+import {
+ Component
+} from '@angular/core';
+
+@Component({
+ selector: 'sky-icon-demo',
+ templateUrl: './icon-demo.component.html'
+})
+export class SkyIconDemoComponent { }
diff --git a/src/demos/icon/index.ts b/src/demos/icon/index.ts
new file mode 100644
index 000000000..5b6cb9876
--- /dev/null
+++ b/src/demos/icon/index.ts
@@ -0,0 +1 @@
+export * from './icon-demo.component';
diff --git a/src/demos/index.ts b/src/demos/index.ts
index 2bcbeddd9..465d6fe7a 100644
--- a/src/demos/index.ts
+++ b/src/demos/index.ts
@@ -17,6 +17,7 @@ export * from './fluid-grid';
export * from './flyout';
export * from './grid';
export * from './help-inline';
+export * from './icon';
export * from './infinite-scroll';
export * from './key-info';
export * from './label';
diff --git a/src/modules/icon/fixtures/icon.component.fixture.html b/src/modules/icon/fixtures/icon.component.fixture.html
new file mode 100644
index 000000000..ed7b283d8
--- /dev/null
+++ b/src/modules/icon/fixtures/icon.component.fixture.html
@@ -0,0 +1,5 @@
+
+
diff --git a/src/modules/icon/fixtures/icon.component.fixture.ts b/src/modules/icon/fixtures/icon.component.fixture.ts
new file mode 100644
index 000000000..6c18e25ef
--- /dev/null
+++ b/src/modules/icon/fixtures/icon.component.fixture.ts
@@ -0,0 +1,13 @@
+import {
+ Component
+} from '@angular/core';
+
+@Component({
+ selector: 'sky-test-cmp',
+ templateUrl: './icon.component.fixture.html'
+})
+export class IconTestComponent {
+ public icon = 'circle';
+ public size = '3x';
+ public fixedWidth = false;
+}
diff --git a/src/modules/icon/icon.component.html b/src/modules/icon/icon.component.html
new file mode 100644
index 000000000..16aefe755
--- /dev/null
+++ b/src/modules/icon/icon.component.html
@@ -0,0 +1,6 @@
+
diff --git a/src/modules/icon/icon.component.spec.ts b/src/modules/icon/icon.component.spec.ts
new file mode 100644
index 000000000..d77d278e7
--- /dev/null
+++ b/src/modules/icon/icon.component.spec.ts
@@ -0,0 +1,68 @@
+import {
+ ComponentFixture,
+ TestBed
+} from '@angular/core/testing';
+
+import {
+ SkyIconModule
+} from './icon.module';
+
+import {
+ IconTestComponent
+} from './fixtures/icon.component.fixture';
+
+import {
+ expect
+} from '@blackbaud/skyux-builder/runtime/testing/browser';
+
+describe('Icon component', () => {
+ let fixture: ComponentFixture;
+ let cmp: IconTestComponent;
+ let element: HTMLElement;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ IconTestComponent
+ ],
+ imports: [
+ SkyIconModule
+ ]
+ });
+
+ fixture = TestBed.createComponent(IconTestComponent);
+ cmp = fixture.componentInstance as IconTestComponent;
+ element = fixture.nativeElement as HTMLElement;
+ });
+
+ it('should display an icon based on the given icon', () => {
+ fixture.detectChanges();
+ expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-circle');
+ expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-3x');
+ expect(element.querySelector('.sky-icon')).not.toHaveCssClass('fa-fw');
+ expect(element.querySelector('.sky-icon').getAttribute('aria-hidden')).toBe('true');
+ expect(element.querySelector('.sky-icon').classList.length).toBe(4);
+ });
+
+ it('should display a different icon with a different size and a fixedWidth', () => {
+ cmp.icon = 'broom';
+ cmp.size = '5x';
+ cmp.fixedWidth = true;
+ fixture.detectChanges();
+ expect(cmp.icon).toBe('broom');
+ expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-broom');
+ expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-5x');
+ expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-fw');
+ expect(element.querySelector('.sky-icon').classList.length).toBe(5);
+ expect(element.querySelector('.sky-icon').getAttribute('aria-hidden')).toBe('true');
+ });
+
+ it('should show an icon without optional inputs', () => {
+ cmp.icon = 'spinner';
+ cmp.size = undefined;
+ cmp.fixedWidth = undefined;
+ fixture.detectChanges();
+ expect(element.querySelector('.sky-icon')).toHaveCssClass('fa-spinner');
+ expect(element.querySelector('.sky-icon').classList.length).toBe(3);
+ });
+});
diff --git a/src/modules/icon/icon.component.ts b/src/modules/icon/icon.component.ts
new file mode 100644
index 000000000..07a9783a2
--- /dev/null
+++ b/src/modules/icon/icon.component.ts
@@ -0,0 +1,33 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Input
+} from '@angular/core';
+
+@Component({
+ selector: 'sky-icon',
+ templateUrl: './icon.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class SkyIconComponent {
+ @Input()
+ public icon: string;
+
+ @Input()
+ public size: string;
+
+ @Input()
+ public fixedWidth: boolean;
+
+ public classList(): string[] {
+ let list: string[] = [];
+ list.push('fa-' + this.icon);
+ if (this.size) {
+ list.push('fa-' + this.size);
+ }
+ if (this.fixedWidth) {
+ list.push('fa-fw');
+ }
+ return list;
+ }
+}
diff --git a/src/modules/icon/icon.module.ts b/src/modules/icon/icon.module.ts
new file mode 100644
index 000000000..8901c16b2
--- /dev/null
+++ b/src/modules/icon/icon.module.ts
@@ -0,0 +1,24 @@
+import {
+ NgModule
+} from '@angular/core';
+
+import {
+ CommonModule
+} from '@angular/common';
+
+import {
+ SkyIconComponent
+} from './icon.component';
+
+@NgModule({
+ declarations: [
+ SkyIconComponent
+ ],
+ imports: [
+ CommonModule
+ ],
+ exports: [
+ SkyIconComponent
+ ]
+})
+export class SkyIconModule { }
diff --git a/src/modules/icon/index.ts b/src/modules/icon/index.ts
new file mode 100644
index 000000000..4837ca018
--- /dev/null
+++ b/src/modules/icon/index.ts
@@ -0,0 +1,2 @@
+export * from './icon.component';
+export * from './icon.module';