diff --git a/package-lock.json b/package-lock.json index ce4c72a58..e7f2b72c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "iso8601-duration": "^1.3.0", "lodash-es": "^4.17.21", "mixpanel-browser": "^2.42.0", + "ngx-color": "7.0.0", "rxjs": "~6.6.7", "tslib": "^2.3.1", "uuid": "^8.3.2", @@ -3789,6 +3790,14 @@ "node": ">=4.0.0" } }, + "node_modules/@ctrl/tinycolor": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz", + "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", @@ -20305,6 +20314,11 @@ "node": ">= 12" } }, + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "node_modules/mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -21056,6 +21070,20 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/ngx-color": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-7.0.0.tgz", + "integrity": "sha512-BiTapBTT/f3sFSEFqet3xe06bpqmBm7hmA24s09ogRYYVGL1J69U13XfLwTQG8PhX4qNBceh7p5nhKA1zczHMg==", + "dependencies": { + "@ctrl/tinycolor": "^3.4.0", + "material-colors": "^1.2.6", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0-0", + "@angular/core": ">=12.0.0-0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", @@ -33006,6 +33034,11 @@ "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", "dev": true }, + "@ctrl/tinycolor": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz", + "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==" + }, "@discoveryjs/json-ext": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", @@ -46187,6 +46220,11 @@ "integrity": "sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw==", "dev": true }, + "material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -46785,6 +46823,16 @@ } } }, + "ngx-color": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-7.0.0.tgz", + "integrity": "sha512-BiTapBTT/f3sFSEFqet3xe06bpqmBm7hmA24s09ogRYYVGL1J69U13XfLwTQG8PhX4qNBceh7p5nhKA1zczHMg==", + "requires": { + "@ctrl/tinycolor": "^3.4.0", + "material-colors": "^1.2.6", + "tslib": "^2.1.0" + } + }, "nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", diff --git a/package.json b/package.json index f105ee1eb..9516ac9e4 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "iso8601-duration": "^1.3.0", "lodash-es": "^4.17.21", "mixpanel-browser": "^2.42.0", + "ngx-color": "7.0.0", "rxjs": "~6.6.7", "tslib": "^2.3.1", "uuid": "^8.3.2", diff --git a/projects/components/package.json b/projects/components/package.json index 7d065a6cb..16f9c2eec 100644 --- a/projects/components/package.json +++ b/projects/components/package.json @@ -27,7 +27,8 @@ "d3-axis": "^2.1.0", "d3-scale": "^3.3.0", "d3-selection": "^1.4.2", - "d3-shape": "^1.3.5" + "d3-shape": "^1.3.5", + "ngx-color": "7.0.0" }, "devDependencies": { "@hypertrace/test-utils": "^0.0.0" diff --git a/projects/components/src/color-picker/color-picker.component.scss b/projects/components/src/color-picker/color-picker.component.scss new file mode 100644 index 000000000..27f4a2327 --- /dev/null +++ b/projects/components/src/color-picker/color-picker.component.scss @@ -0,0 +1,31 @@ +@import 'color-palette'; +@import 'mixins'; + +.color-picker { + display: flex; + flex-flow: row wrap; + align-items: center; + + .color { + width: 24px; + height: 24px; + border-radius: 50%; + margin-right: 6px; + cursor: pointer; + + &.selected { + border: 2px solid $blue-4; + } + } +} + +.container { + width: 200px; + height: 200px; + cursor: pointer; +} + +.color-sketch { + width: 200px; + height: 200px; +} diff --git a/projects/components/src/color-picker/color-picker.component.test.ts b/projects/components/src/color-picker/color-picker.component.test.ts new file mode 100644 index 000000000..0481c3111 --- /dev/null +++ b/projects/components/src/color-picker/color-picker.component.test.ts @@ -0,0 +1,54 @@ +import { CommonModule } from '@angular/common'; +import { fakeAsync } from '@angular/core/testing'; +import { IconType } from '@hypertrace/assets-library'; +import { Color, NavigationService } from '@hypertrace/common'; +import { IconComponent } from '@hypertrace/components'; +import { createHostFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; +import { SketchComponent } from 'ngx-color/sketch'; +import { NEVER, Observable } from 'rxjs'; +import { NotificationService } from '../notification/notification.service'; +import { PopoverModule } from '../popover/popover.module'; +import { ColorPickerComponent } from './color-picker.component'; + +describe('Color Picker component', () => { + let spectator: Spectator; + + const createHost = createHostFactory({ + component: ColorPickerComponent, + imports: [CommonModule, PopoverModule], + providers: [ + mockProvider(NotificationService, { withNotification: (x: Observable) => x }), + mockProvider(NavigationService, { + navigation$: NEVER + }) + ], + declarations: [MockComponent(SketchComponent), MockComponent(IconComponent)] + }); + + test('should render color picker with default colors', fakeAsync(() => { + const onSelectionChangeSpy = jest.fn(); + spectator = createHost( + ` + `, + { + hostProps: { + onSelectionChange: onSelectionChangeSpy + } + } + ); + + const colors = spectator.queryAll('.color-picker .color'); + expect(colors.length).toBe(7); + expect(spectator.query(IconComponent)?.icon).toBe(IconType.Add); + + spectator.click(colors[1]); + expect(spectator.component.selected).toEqual(Color.Blue3); + expect(onSelectionChangeSpy).toHaveBeenCalledWith(Color.Blue3); + + spectator.click('.add-icon'); + spectator.tick(); + + expect(spectator.query('.color-sketch', { root: true })).toExist(); + })); +}); diff --git a/projects/components/src/color-picker/color-picker.component.ts b/projects/components/src/color-picker/color-picker.component.ts new file mode 100644 index 000000000..7c3c1244e --- /dev/null +++ b/projects/components/src/color-picker/color-picker.component.ts @@ -0,0 +1,89 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { IconType } from '@hypertrace/assets-library'; +import { Color } from '@hypertrace/common'; +import { IconSize } from '../icon/icon-size'; + +@Component({ + selector: 'ht-color-picker', + styleUrls: ['./color-picker.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: ColorPickerComponent + } + ], + template: ` +
+
+ + + + + +
+ +
+
+
+
+ ` +}) +export class ColorPickerComponent { + @Input() + public selected?: string; + + @Output() + private readonly selectedChange: EventEmitter = new EventEmitter(); + + private readonly paletteSet: Set = new Set([ + Color.Brown1, + Color.Blue3, + Color.Green3, + Color.Orange3, + Color.Purple3, + Color.Red3, + Color.Yellow3 + ]); + public paletteColors: string[] = Array.from(this.paletteSet); + + private propagateControlValueChange?: (value: string | undefined) => void; + private propagateControlValueChangeOnTouch?: (value: string | undefined) => void; + + public onAddColorToPalette(color: string): void { + this.paletteSet.add(color); + this.paletteColors = Array.from(this.paletteSet); + this.selectColor(color); + } + + public selectColor(color: string): void { + this.selected = color; + this.selectedChange.emit(color); + this.propagateValueChangeToFormControl(color); + } + + private propagateValueChangeToFormControl(value?: string): void { + this.propagateControlValueChange?.(value); + this.propagateControlValueChangeOnTouch?.(value); + } + + public writeValue(color?: string): void { + this.selected = color; + } + + public registerOnChange(onChange: (value?: string) => void): void { + this.propagateControlValueChange = onChange; + } + + public registerOnTouched(onTouch: (value?: string) => void): void { + this.propagateControlValueChangeOnTouch = onTouch; + } +} diff --git a/projects/components/src/color-picker/color-picker.module.ts b/projects/components/src/color-picker/color-picker.module.ts new file mode 100644 index 000000000..d921cfb4d --- /dev/null +++ b/projects/components/src/color-picker/color-picker.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ColorSketchModule } from 'ngx-color/sketch'; +import { IconModule } from '../icon/icon.module'; +import { PopoverModule } from './../popover/popover.module'; +import { ColorPickerComponent } from './color-picker.component'; + +@NgModule({ + imports: [CommonModule, FormsModule, IconModule, ColorSketchModule, PopoverModule], + declarations: [ColorPickerComponent], + exports: [ColorPickerComponent] +}) +export class ColorPickerModule {} diff --git a/projects/components/src/form-field/form-field.component.scss b/projects/components/src/form-field/form-field.component.scss index e6a0afe3b..32b1c663b 100644 --- a/projects/components/src/form-field/form-field.component.scss +++ b/projects/components/src/form-field/form-field.component.scss @@ -31,6 +31,7 @@ .content { flex: 1 1 auto; + display: flex; width: 100%; background: white; diff --git a/projects/components/src/public-api.ts b/projects/components/src/public-api.ts index 374277bae..3d030055d 100644 --- a/projects/components/src/public-api.ts +++ b/projects/components/src/public-api.ts @@ -37,6 +37,10 @@ export * from './tabs/content/tab-group.component'; export * from './tabs/content/tab.module'; export * from './tabs/content/tab/tab.component'; +// Color picker +export * from './color-picker/color-picker.component'; +export * from './color-picker/color-picker.module'; + // Copy to Clipboard export * from './copy-to-clipboard/copy-to-clipboard.component'; export * from './copy-to-clipboard/copy-to-clipboard.module'; diff --git a/tsconfig.base.json b/tsconfig.base.json index 374e43474..141bd04e7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,6 +16,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "downlevelIteration": true, + "allowJs": true, "lib": ["es2015", "es2016", "es2017", "esnext.string", "esnext.array", "esnext.asynciterable", "dom"], "paths": { "@hypertrace/assets-library": ["projects/assets-library/src/public-api.ts"],