Skip to content

Commit

Permalink
feat(color-picker): add color picker (#444)
Browse files Browse the repository at this point in the history
* feat(color-picker): add color picker

* fix(color-dialog): import test path

* chore(color-picker): update code style

* fix(color-picker): update snapshot ignore style

* fix(color-picker): add missing end tag

* chore(color-picker): rename test file
  • Loading branch information
Nantawat-Poothong authored Aug 31, 2022
1 parent edcc6df commit 3718003
Show file tree
Hide file tree
Showing 23 changed files with 664 additions and 26 deletions.
56 changes: 56 additions & 0 deletions documents/src/pages/elements/color-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!--
type: page
title: Color Picker
location: ./elements/color-picker
layout: default
-->

# Color Picker
::
```javascript
::color-picker::
```
```css
<style>
section {
display:flex;
justify-content: left;
align-items: baseline;
height: 380px;
padding: 4px;
}
ef-color-picker {
margin-right: 2px;
}
</style>
```
```html
<section>
<ef-color-picker></ef-color-picker>
<ef-color-picker value="#ff3300"></ef-color-picker>
<ef-color-picker value="#00ff99"></ef-color-picker>
<ef-color-picker value="#0066ff" opened></ef-color-picker>
</section>
```
::

`ef-color-picker` allows users to pick any colours from colour dialog.

### Basic usage
You can set an initial value via `value` attribute. The `value` must be a string of hex colour code.

```html
<ef-color-picker value="#001EFF"></ef-color-picker>
```

### Getting value
A value of Color picker can be accessed through `value` property. It will fire `value-changed` event when users picked a new colour. `value` will be an empty string if users choose No Color.

### 'No Color' option
In some circumstances, it might be necessary that the component should allow user to select "no color". This can be done by using a property/attribute `allow-nocolor` to activate this feature.

Color picker will set attribute/property `value` to `""` when users select no-color from the colour dialog.

```html
<ef-color-picker allow-nocolor></ef-color-picker>
```
33 changes: 33 additions & 0 deletions packages/elemental-theme/src/custom-elements/ef-color-picker.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

@import 'element:ef-color-dialog';
@import '../responsive';

:host {
@color-picker-size: @button-height;
@color-picker-border: @control-border-color;
@color-picker-focused-border-color: @scheme-color-primary;

outline: none;
box-sizing: border-box;
height: @color-picker-size;
min-width: @color-picker-size;
border: 1px solid @color-picker-border;

&[disabled], &[readonly] {
border-color: @input-disabled-border-color;
}

[part="color-item"] {
cursor: pointer;
height: 100%;
}

&[disabled] [part="color-item"],
&[readonly] [part="color-item"] {
pointer-events: none;
}

&[focused=visible] {
border-color: @color-picker-focused-border-color;
}
}
5 changes: 5 additions & 0 deletions packages/elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
"./color-dialog/themes/halo/light": "./lib/color-dialog/themes/halo/light/index.js",
"./color-dialog/themes/solar/charcoal": "./lib/color-dialog/themes/solar/charcoal/index.js",
"./color-dialog/themes/solar/pearl": "./lib/color-dialog/themes/solar/pearl/index.js",
"./color-picker": "./lib/color-picker/index.js",
"./color-picker/themes/halo/dark": "./lib/color-picker/themes/halo/dark/index.js",
"./color-picker/themes/halo/light": "./lib/color-picker/themes/halo/light/index.js",
"./color-picker/themes/solar/charcoal": "./lib/color-picker/themes/solar/charcoal/index.js",
"./color-picker/themes/solar/pearl": "./lib/color-picker/themes/solar/pearl/index.js",
"./combo-box": "./lib/combo-box/index.js",
"./combo-box/themes/halo/dark": "./lib/combo-box/themes/halo/dark/index.js",
"./combo-box/themes/halo/light": "./lib/combo-box/themes/halo/light/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import '@refinitiv-ui/elements/color-dialog';
import '@refinitiv-ui/elemental-theme/light/ef-color-dialog';
import '@refinitiv-ui/elemental-theme/light/ef-text-field';
import '@refinitiv-ui/elemental-theme/light/ef-number-field';
import { rgb } from '@refinitiv-ui/utils';
import { COLOR_ITEMS, removeHashSign } from '../../../lib/color-dialog/helpers/color-helpers';
import { rgb, removeHashSign } from '@refinitiv-ui/utils/color.js';
import { COLOR_ITEMS } from '../../../lib/color-dialog/helpers/color-helpers';

describe('color-dialog/ColorDialog', () => {
describe('Default Color Dialog', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isHex } from '../../../lib/color-dialog/helpers/color-helpers.js';
import { isHex } from '@refinitiv-ui/utils/color.js';
import { expect } from '@refinitiv-ui/test-helpers';

describe('color-dialog/Helpers', () => {
Expand Down
3 changes: 1 addition & 2 deletions packages/elements/src/color-dialog/elements/palettes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import {
import { property } from '@refinitiv-ui/core/decorators/property.js';
import { query } from '@refinitiv-ui/core/decorators/query.js';
import { VERSION } from '../../version.js';
import { rgb } from '@refinitiv-ui/utils/color.js';
import { isHex } from '../helpers/color-helpers.js';
import { rgb, isHex } from '@refinitiv-ui/utils/color.js';

/**
* Element base class usually used
Expand Down
17 changes: 0 additions & 17 deletions packages/elements/src/color-dialog/helpers/color-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,3 @@ export const GRAYSCALE_ITEMS = [
];

export const NOCOLOR_POINTS = '6, 2, 15, 6, 15, 17, 6, 21, -3, 17, -3, 6';

const HEX_REGEXP = /^#([0-9A-F]{3}){1,2}$/i; // used to validate HEX
export const isHex = (value: string): boolean => HEX_REGEXP.test(value);

/**
* Remove hash (#) sign from hex value
* @param hex Hex to check
* @returns hex value without # sign
*/
export const removeHashSign = (hex: string): string => {
if (hex) {
if (hex.startsWith('#')) {
hex = hex.slice(1);
}
}
return hex;
};
3 changes: 1 addition & 2 deletions packages/elements/src/color-dialog/helpers/value-model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { rgb } from '@refinitiv-ui/utils/color.js';
import { isHex } from './color-helpers.js';
import { rgb, isHex } from '@refinitiv-ui/utils/color.js';

const rgbNumberToString = (value: number): string => isNaN(value) ? '' : `${value}`; // replace NaN with empty string

Expand Down
3 changes: 1 addition & 2 deletions packages/elements/src/color-dialog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import { customElement } from '@refinitiv-ui/core/decorators/custom-element.js';
import { property } from '@refinitiv-ui/core/decorators/property.js';
import { query } from '@refinitiv-ui/core/decorators/query.js';
import { styleMap } from '@refinitiv-ui/core/directives/style-map.js';
import { rgb } from '@refinitiv-ui/utils/color.js';
import { rgb, isHex, removeHashSign } from '@refinitiv-ui/utils/color.js';
import { VERSION } from '../version.js';
import type { NumberField } from '../number-field';
import type { TextField } from '../text-field';
import type { Palettes } from './elements/palettes';
import { ValueModel } from './helpers/value-model.js';
import { isHex, removeHashSign } from './helpers/color-helpers.js';
import '../button/index.js';
import '../number-field/index.js';
import '../text-field/index.js';
Expand Down
65 changes: 65 additions & 0 deletions packages/elements/src/color-picker/__demo__/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Color Picker</title>
<link rel="stylesheet" href="/node_modules/@refinitiv-ui/demo-block/demo.css">
</head>
<body>
<script type="module">
import '@refinitiv-ui/phrasebook/locale/de/color-picker.js';
import '@refinitiv-ui/phrasebook/locale/ja/color-picker.js';
import '@refinitiv-ui/phrasebook/locale/zh/color-picker.js';
import '@refinitiv-ui/phrasebook/locale/zh-hant/color-picker.js';
</script>
<script type="module">
import '@refinitiv-ui/elements/color-picker';
import '@refinitiv-ui/demo-block';
</script>
<style>
.output {
width: 100%;
height: 100px;
}

#hexInputDefault {
width: 100px;
}

textarea {
height: 100px;
width: 100%;
resize: none;
}
</style>
<demo-block header="Default" layout="normal" tags="default">
<ef-color-picker id="defaultColorPicker"></ef-color-picker>
<div>
Default Hex Value (Optional): <input id="hexInputDefault" class="hex-input" value="">
</div>
<div>
<p> Output: </p>
<textarea readonly id="defaultColorOutput"></textarea>
</div>
<script>
// Default Color Picker
const defaultColorPicker = document.getElementById('defaultColorPicker');
const hexInputDefault = document.getElementById('hexInputDefault');
const defaultColorOutput = document.getElementById('defaultColorOutput');
hexInputDefault.addEventListener('change', event => {
defaultColorPicker.setAttribute('value', hexInputDefault.value);
});
defaultColorPicker.addEventListener('value-changed', event => {
const value = event.detail.value;
hexInputDefault.value = value;
defaultColorOutput.value += 'Value Change : ' + value + '\n';
});
</script>
</demo-block>
<demo-block header="Readonly/Disabled" layout="normal" tags="readonly,disabled">
<ef-color-picker readonly value="#ffffcc"></ef-color-picker>
<ef-color-picker disabled value="#ffffcc"></ef-color-picker>
</demo-block>
</body>
</html>
42 changes: 42 additions & 0 deletions packages/elements/src/color-picker/__snapshots__/ColorPicker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# `color-picker/ColorPicker`

## `DOM structure`

#### `DOM structure is correct`

```html
<div
part="color-item"
style="background-color:#001EFF;"
>
</div>

```

#### `DOM structure is correct when opened`

```html
<div
part="color-item"
style="background-color:#001EFF;"
>
</div>
<ef-overlay-viewport>
</ef-overlay-viewport>
<ef-overlay-backdrop style="z-index: 103;">
</ef-overlay-backdrop>
<ef-color-dialog
aria-modal="true"
draggable=""
offset="4"
opened=""
part="dialog"
role="dialog"
style="z-index: 103; pointer-events: auto;"
tabindex="-1"
with-shadow=""
>
</ef-color-dialog>

```

114 changes: 114 additions & 0 deletions packages/elements/src/color-picker/__test__/color-picker.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { fixture, expect, elementUpdated, keyboardEvent, oneEvent } from '@refinitiv-ui/test-helpers';
// import element and theme
import '@refinitiv-ui/elements/color-picker';
import '@refinitiv-ui/elemental-theme/light/ef-color-picker';

describe('color-picker/ColorPicker', () => {

describe('DOM structure', () => {
it('DOM structure is correct', async () => {
const el = await fixture('<ef-color-picker value="#001EFF"></ef-color-picker>');
expect(el).shadowDom.to.equalSnapshot();
});
it('DOM structure is correct when opened', async () => {
const el = await fixture('<ef-color-picker value="#001EFF" opened></ef-color-picker>');
expect(el).shadowDom.to.equalSnapshot({ ignoreAttributes: ['class', 'style'] });
});
});

describe('Value property', () => {
it('Should have default value', async () => {
const el = await fixture('<ef-color-picker></ef-color-picker>');
expect(el.value).to.equal('');
});
it('Should update value when set hex color', async () => {
const el = await fixture('<ef-color-picker value="#001EFF"></ef-color-picker>');
expect(el.value).to.equal('#001EFF');
});
it('Should reset to default value when value is invalid', async () => {
const el = await fixture('<ef-color-picker value="hello"></ef-color-picker>');
expect(el.value).to.equal('');
});
it("Should not fires value-changed event when programmatically changes value", async () => {
const value = '#001EFF';
const el = await fixture('<ef-color-picker></ef-color-picker>');

let eventFired = false;
el.addEventListener('value-changed', () => {
eventFired = true;
});
el.value = value;
expect(el.value).to.equal(value);
expect(eventFired).to.equal(false);
});
it('Should fires value-changed event when value change by user interactions', async () => {
const el = await fixture('<ef-color-picker value="#001EFF" opened></ef-color-picker>');
const dialogEl = el.dialogEl;
const redInput = dialogEl.shadowRoot.getElementById('redInput');
const confirmBtn = dialogEl.shadowRoot.getElementById('confirmButton');
redInput.value = 200;
redInput.dispatchEvent(new Event('value-changed'));
await elementUpdated();
setTimeout(() => confirmBtn.click());
await oneEvent(el, 'value-changed');
await elementUpdated();
expect(el.value).to.equal('#c81eff');
});
});

describe('No color property', () => {
it('Should not have allow-nocolor property on color dialog', async () => {
const el = await fixture('<ef-color-picker></ef-color-picker>');
el.opened = true;
await elementUpdated(el);
expect(el.dialogEl.hasAttribute('allow-nocolor')).to.be.equal(false);
});
it('Should pass allow-nocolor property to color dialog', async () => {
const el = await fixture('<ef-color-picker allow-nocolor></ef-color-picker>');
el.opened = true;
await elementUpdated(el);
expect(el.dialogEl.hasAttribute('allow-nocolor')).to.be.equal(true);
});
});

describe('Color dialog', () => {
it('Should open dialog when click on color picker', async () => {
const el = await fixture('<ef-color-picker></ef-color-picker>');
el.click();
await elementUpdated(el);
expect(el.dialogEl.opened).to.be.equal(true, 'clicking on color picker should open color dialog');
});
it('Should open dialog when opened programmatically', async () => {
const el = await fixture('<ef-color-picker></ef-color-picker>');
el.opened = true;
await elementUpdated(el);
expect(el.dialogEl.hasAttribute('opened')).to.be.equal(true);
});
it('Should not open color dialog when disabled', async () => {
const el = await fixture('<ef-color-picker disabled></ef-color-picker>');
el.click();
expect(el.opened).to.be.equal(false, 'clicking on disabled should do nothing');
});
it('Should not open color dialog when readonly', async () => {
const el = await fixture('<ef-color-picker readonly></ef-color-picker>');
el.click();
expect(el.opened).to.be.equal(false, 'clicking on readonly should do nothing');
});
});

describe('Navigation', () => {
it('Should open dialog when press enter key', async () => {
const el = await fixture('<ef-color-picker></ef-color-picker>');
el.dispatchEvent(keyboardEvent('keydown', { key: 'Enter' }));
await elementUpdated(el);
expect(el.dialogEl.opened).to.be.equal(true, 'Enter should open dialog');
});
it('Should open dialog when press spacebar key', async () => {
const el = await fixture('<ef-color-picker></ef-color-picker>');
el.dispatchEvent(keyboardEvent('keydown', { key: 'Spacebar' }));
await elementUpdated(el);
expect(el.dialogEl.opened).to.be.equal(true, 'Spacebar should open dialog');
});
});
});

Loading

0 comments on commit 3718003

Please sign in to comment.