Skip to content

Commit

Permalink
[space-picker] First iteration (#143)
Browse files Browse the repository at this point in the history
* Use `value` for actual input (as a string) and `selectedSpace` for the corresponding space object
* Support custom grouping of color spaces via the `groups()` prop
* Don't drop non-existent spaces; replace them with the `{ id, name: id }` objects
* Expose the `valuechange` and `spacechange` events
* Retarget the `input` event
* In `formAssociated`, use `change` as the `changeEvent`

* Add the `groupBy()` prop that expects a space and returns the name of a group the space belongs to
* Related to the previous one: `groups` is now a computed prop returning an object with all the groups
* Use different UI for cases with grouping and without

* Add `<space-picker>` to the list of upcoming elements

* Add basic docs
  • Loading branch information
DmitrySharabin authored Nov 5, 2024
1 parent 0813ef7 commit 1075f15
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Use at your own risk, the API can change at any point.
### Upcoming:

- `<color-plane>`
- `<space-picker>`

## Usage

Expand Down
103 changes: 103 additions & 0 deletions src/space-picker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# `<space-picker>`

## Usage

### Basic usage

```html
<space-picker value="oklab"></space-picker>
```

If no color space is provided (via the `value` attribute/property),
the first one will be used:

```html
<space-picker></space-picker>
```

You can specify what color spaces to use:
```html
<space-picker spaces="oklch, p3, srgb" value="p3"></space-picker>
```

Unknown color spaces also work:
```html
<space-picker spaces="bar, oklch, p3, srgb, foo" value="foo"></space-picker>
```

### Grouping the color spaces

You can group the color spaces the way you like by specifying the `groupBy` property. Its value is a function
accepting a color space as an argument and returning the name of a group the color space should be added to:

```html
<space-picker id="space_picker" spaces="oklch, p3, srgb" value="p3"></space-picker>
<script>
space_picker.groupBy = (space) => {
let isPolar = space.coords.h?.type === "angle";
return isPolar ? "Polar" : "Rectangular";
};
</script>
```
### Events
You can listen to the `spacechange` event to get either the id of the current color space (the `value` property)
or the color space object itself (the `selectedSpace` property):
```html
<space-picker onspacechange="this.nextElementSibling.textContent = this.value"></space-picker>
<output></output>
```
### Dynamic
All properties are reactive and can be set programmatically:
```html
<button onclick="this.nextElementSibling.value = 'oklch'">Switch to Oklch</button>
<space-picker value="p3"></space-picker>
```
`<space-picker>` plays nicely with other color elements:
```html
<label>
Space:
<space-picker value="oklch" onspacechange="this.parentElement.nextElementSibling.space = this.selectedSpace"></space-picker>
</label>
<color-picker color="oklch(60% 30% 180)"></color-picker>
```
## Reference
### Attributes & Properties
| Attribute | Property | Property type | Default value | Description |
|-----------|-----------|-------------------------------------|-----------------------------------------|-----------------------------------------------------------------------------------------------------------|
| `value` | `value` | `string` | The first color space in `this.spaces`. | The current value of the picker. |
| `spaces` | `spaces` | `string` &#124; `Array<ColorSpace>` | All known color spaces. | Comma-separated list of color spaces to use. |
| — | `groupBy` | `Function` | — | Function to group the color spaces. Takes a color space object as an argument and returns the group name. |
### Getters
These properties are read-only.
| Property | Type | Description |
|-----------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `selectedSpace` | `ColorSpace` | Color space object corresponding to the picker current value. |
| `groups` | `Object` | Object containing the color spaces grouped by the `groupBy()` function. Keys are group names, values are objects with space ids as keys, and corresponding color space objects are values. |
### Events
| Name | Description |
|---------------|------------------------------------------------------------------------------|
| `input` | Fired when the space changes due to user action. |
| `change` | Fired when the space changes due to user action. |
| `valuechange` | Fired when the value changes for any reason, and once during initialization. |
| `spacechange` | Fired when the space changes for any reason, and once during initialization. |
### Parts
| Name | Description |
|----------|----------------------------------|
| `picker` | The internal `<select>` element. |
7 changes: 7 additions & 0 deletions src/space-picker/space-picker.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#picker {
font: inherit;
color: inherit;
background: inherit;
field-sizing: content;
cursor: pointer;
}
197 changes: 197 additions & 0 deletions src/space-picker/space-picker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import ColorElement from "../common/color-element.js";

const Self = class SpacePicker extends ColorElement {
static tagName = "space-picker";
static url = import.meta.url;
static shadowStyle = true;
static shadowTemplate = `<select id="picker" part="picker"></select>`;

constructor () {
super();

this._el = {};
this._el.picker = this.shadowRoot.querySelector("#picker");
}

connectedCallback () {
super.connectedCallback?.();
this._el.picker.addEventListener("change", this);
}

disconnectedCallback () {
super.disconnectedCallback?.();
this._el.picker.removeEventListener("change", this);
}

handleEvent (event) {
if (event.type === "change" && event.target === this._el.picker && event.target.value !== this.value) {
this.value = event.target.value;
}
}

propChangedCallback ({name, prop, detail: change}) {
if (name === "spaces") {
if (!this.groups) {
this._el.picker.innerHTML = Object.entries(this.spaces)
.map(([id, space]) => `<option value="${id}">${space.name}</option>`)
.join("\n");
}
else {
let groups = this.groups;

// Remove empty groups
groups = Object.entries(groups).filter(([type, spaces]) => {
if (Object.keys(spaces).length === 0) {
console.warn(`Removed empty group of color spaces with the label "${type}."`);
return false;
}

return true;
});

if (!groups.length) {
console.warn("All provided groups of color spaces are empty. Falling back to default grouping.");
groups = [["All spaces", this.spaces]];
}

this._el.picker.innerHTML = groups.map(([type, spaces]) => `
<optgroup label="${type}">
${Object.entries(spaces)
.map(([id, space]) => `<option value="${id}">${space.name}</option>`)
.join("\n")}
</optgroup>
`).join("\n");
}

this._el.picker.value = this.value;
}

if (name === "value") {
let value = this.value;

if (value !== undefined && value !== null) {
if (!(value in this.spaces)) {
let spaces = Object.keys(this.spaces).join(", ");
console.warn(`No color space found with id = "${ value }". Choose one of the following: ${ spaces }.`);
}

if (this._el.picker.value !== value) {
this._el.picker.value = value;
}
}
}
}

static props = {
value: {
default () {
if (this.groups) {
let groups = this.groups;
let firstGroup = Object.values(groups)[0];

return firstGroup && Object.keys(firstGroup)[0];
}
else {
return Object.keys(this.spaces)[0];
}
},
},

selectedSpace: {
get () {
let value = this.value;
if (value === undefined || value === null) {
return;
}

return this.spaces[value];
},
},

spaces: {
type: {
is: Object,
get values () {
return Self.Color.Space;
},
defaultValue: (id, index) => {
try {
return Self.Color.Space.get(id);
}
catch (e) {
console.error(e);
}
},
},
default: () => Self.Color.spaces,
convert (value) {
// Replace non-existing spaces with { id, name: id }
for (let id in value) {
if (!value[id]) {
value[id] = { id, name: id };
}
}

return value;
},
stringify (value) {
return Object.entries(value).map(([id, space]) => id).join(", ");
},
},

groupBy: {
type: {
is: Function,
arguments: ["space"],
},
reflect: false,
},

groups: {
get () {
if (!this.groupBy) {
return;
}

let ret = {};
for (let [id, space] of Object.entries(this.spaces)) {
let group = this.groupBy(space);
if (group) {
(ret[group] ??= {})[id] = space;
}
}

return ret;
},
},
};

static events = {
change: {
from () {
return this._el.picker;
},
},
input: {
from () {
return this._el.picker;
},
},
valuechange: {
propchange: "value",
},
spacechange: {
propchange: "selectedSpace",
},
};

static formAssociated = {
like: el => el._el.picker,
role: "combobox",
changeEvent: "change",
};
};

Self.define();

export default Self;

0 comments on commit 1075f15

Please sign in to comment.