WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit f0f8317

Browse files
authored
Merge pull request #76 from IATI/ml/form-elements
Add select and multi-select form elements
2 parents a879403 + a1f27b7 commit f0f8317

File tree

9 files changed

+491
-59
lines changed

9 files changed

+491
-59
lines changed

package-lock.json

Lines changed: 152 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/scss/components/_index.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
@forward "country-switcher/country-switcher";
77
@forward "data-card/data-card";
88
@forward "figures/figures";
9+
@forward "form/form";
910
@forward "piped-list/piped-list";
1011
@forward "icon/icon";
1112
@forward "jump-menu/jump-menu";
1213
@forward "search-bar/search-bar";
1314
@forward "section/section";
15+
@forward "select/select";
1416
@forward "table/table";
1517
@forward "menu-toggle/menu-toggle";
1618
@forward "message/message";

src/scss/components/button/_button.scss

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,40 @@
5151
padding-top: 0.5rem;
5252
padding-bottom: 0.5rem;
5353
}
54+
55+
&--pill {
56+
color: $color-grey-90;
57+
background-color: $color-teal-10;
58+
border: 1px solid $color-teal-50;
59+
padding-top: 0.5rem;
60+
padding-bottom: 0.5rem;
61+
&:hover {
62+
background-color: white;
63+
}
64+
}
65+
66+
&--remove {
67+
position: relative;
68+
padding-right: 2.1em;
69+
70+
&::after {
71+
position: absolute;
72+
right: 0.7em;
73+
top: 50%;
74+
transform: translateY(-50%);
75+
content: "×";
76+
color: currentColor;
77+
pointer-events: none;
78+
padding-bottom: 0.1rem;
79+
font-size: 1em;
80+
transition: all 0.2s ease-in-out;
81+
}
82+
83+
&:hover {
84+
&::after {
85+
color: $color-red-40;
86+
transition: all 0.2s ease-in-out;
87+
}
88+
}
89+
}
5490
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
@use "../../tokens/color" as *;
2+
@use "../../tokens/font" as *;
3+
@use "../../base/mixins";
4+
@use "../../tokens/screens" as *;
5+
6+
.iati-form {
7+
color: $color-grey-90;
8+
9+
min-width: min(31.25rem, calc(100vw - 2rem));
10+
@media (min-width: $screen-md) {
11+
grid-template-columns: max-content 1fr;
12+
}
13+
14+
&__inner {
15+
background-color: $color-teal-20;
16+
border: 1px solid $color-teal-50;
17+
display: grid;
18+
gap: 1rem;
19+
padding: 1rem;
20+
}
21+
22+
&__item {
23+
display: grid;
24+
grid-template-columns: subgrid;
25+
grid-column: 1 / -1;
26+
align-items: start;
27+
gap: 0.2rem;
28+
@media (min-width: $screen-md) {
29+
gap: 1rem;
30+
}
31+
}
32+
33+
&__label {
34+
font-weight: 600;
35+
grid-column: 1 / -1;
36+
@media (min-width: $screen-md) {
37+
text-align: start;
38+
}
39+
}
40+
41+
&__input {
42+
max-width: 100%;
43+
border: 1px solid $color-teal-50;
44+
background-color: #fff;
45+
padding: 0.5em 1em;
46+
grid-column: -1 / 1;
47+
}
48+
49+
&__submit {
50+
margin: 1rem 0;
51+
max-width: 100%;
52+
min-width: 6rem;
53+
}
54+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { Meta, StoryObj } from "@storybook/web-components";
2+
import { html } from "lit";
3+
4+
import "../select/multi-select.ts";
5+
6+
const meta: Meta = {
7+
title: "Components/Form",
8+
parameters: {
9+
backgrounds: {
10+
default: "light",
11+
},
12+
},
13+
};
14+
15+
export default meta;
16+
type Story = StoryObj;
17+
18+
export const Default: Story = {
19+
render: () => html`
20+
<form class="iati-form">
21+
<div class="iati-form__inner">
22+
<div class="iati-form__item">
23+
<label for="search-input" class="iati-form__label"
24+
>Search for items containing:</label
25+
>
26+
<input id="search-input" type="text" class="iati-form__input" />
27+
</div>
28+
<div class="iati-form__item">
29+
<div class="iati-select">
30+
<label for="iati-single-select" class="iati-select__label">
31+
Choose your language
32+
</label>
33+
<div class="iati-select__control-wrapper">
34+
<select id="iati-single-select" class="iati-select__control">
35+
<option>English</option>
36+
<option>French</option>
37+
</select>
38+
</div>
39+
</div>
40+
</div>
41+
<div class="iati-form__item">
42+
<multi-select
43+
.label=${"Choose your language(s)"}
44+
.options=${["English", "French"]}
45+
></multi-select>
46+
</div>
47+
</div>
48+
<button class="iati-form__submit iati-button iati-button--submit">
49+
Search
50+
</button>
51+
</form>
52+
`,
53+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
@use "../../tokens/color" as *;
2+
@use "../../tokens/font" as *;
3+
4+
.iati-select {
5+
color: $color-grey-90;
6+
7+
&__label {
8+
display: block;
9+
margin: 0 0 1rem 0;
10+
}
11+
12+
&__control-wrapper {
13+
position: relative;
14+
width: 100%;
15+
16+
&::after {
17+
position: absolute;
18+
right: 0.7em;
19+
top: calc(50% - 0.1875em);
20+
content: "";
21+
width: 0.75em;
22+
height: 0.375em;
23+
background-color: currentColor;
24+
clip-path: polygon(15% 0, 0 0, 50% 100%, 100% 0, 85% 0, 50% 70%);
25+
pointer-events: none;
26+
}
27+
}
28+
29+
&__control {
30+
display: inline-block;
31+
padding: 0.7em 2.1em 0.7em 0.7em;
32+
font-family: $font-stack-heading;
33+
line-height: 1.1;
34+
width: 100%;
35+
height: 100%;
36+
appearance: none;
37+
background-color: #fff;
38+
border: 1px solid $color-teal-50;
39+
font-family: $font-stack-heading;
40+
color: $color-grey-90;
41+
font-weight: 600;
42+
}
43+
}
44+
45+
.iati-multi-select {
46+
margin: 1rem 0 0 0;
47+
padding: 0;
48+
49+
&__item {
50+
--display: inline-flex; // Used by .display--* utilities
51+
52+
display: var(--display);
53+
align-items: center;
54+
justify-content: center;
55+
gap: 0.25em;
56+
margin: 0;
57+
padding: 0;
58+
}
59+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { LitElement, html } from "lit";
2+
import { customElement, property, state } from "lit/decorators.js";
3+
import { map } from "lit/directives/map.js";
4+
5+
@customElement("multi-select")
6+
export class MultiSelect extends LitElement {
7+
@property({ type: String })
8+
label: string = "Select options";
9+
10+
@property({ type: Array })
11+
options: string[] = [];
12+
13+
@state()
14+
selectedItems: string[] = [];
15+
16+
createRenderRoot() {
17+
return this;
18+
}
19+
20+
_handleSelect(e: Event) {
21+
const selectEl = e.target as HTMLSelectElement;
22+
const value = selectEl.value;
23+
24+
if (value && !this.selectedItems.includes(value)) {
25+
this.selectedItems = [...this.selectedItems, value];
26+
}
27+
selectEl.value = "";
28+
}
29+
30+
_handleRemove(itemToRemove: string) {
31+
this.selectedItems = this.selectedItems.filter(
32+
(item) => item !== itemToRemove,
33+
);
34+
}
35+
36+
render() {
37+
const availableOptions = this.options.filter(
38+
(opt) => !this.selectedItems.includes(opt),
39+
);
40+
41+
return html`
42+
<div class="iati-select">
43+
<label for="iati-multi-select" class="iati-select__label"
44+
>${this.label}</label
45+
>
46+
47+
<div class="iati-select__control-wrapper">
48+
<select
49+
id="iati-multi-select"
50+
class="iati-select__control"
51+
@change=${this._handleSelect}
52+
>
53+
<option value="">---</option>
54+
${map(
55+
availableOptions,
56+
(opt) => html`<option value=${opt}>${opt}</option>`,
57+
)}
58+
</select>
59+
</div>
60+
</div>
61+
62+
<ul class="iati-multi-select">
63+
${map(
64+
this.selectedItems,
65+
(item) => html`
66+
<li class="iati-multi-select__item">
67+
<button
68+
class="iati-button iati-button--pill iati-button--remove"
69+
@click=${() => this._handleRemove(item)}
70+
title="Remove ${item}"
71+
>
72+
${item}
73+
</button>
74+
</li>
75+
`,
76+
)}
77+
</ul>
78+
`;
79+
}
80+
}
81+
82+
declare global {
83+
interface HTMLElementTagNameMap {
84+
"multi-select": MultiSelect;
85+
}
86+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Meta, StoryObj } from "@storybook/web-components";
2+
import { html } from "lit";
3+
4+
import "./multi-select.ts";
5+
6+
const meta: Meta = {
7+
title: "Components/Select",
8+
parameters: {
9+
backgrounds: {
10+
default: "light",
11+
},
12+
},
13+
argTypes: {
14+
label: { control: "text" },
15+
options: { control: "object" },
16+
},
17+
};
18+
19+
export default meta;
20+
type Story = StoryObj;
21+
22+
export const SingleSelect: Story = {
23+
name: "Single Select",
24+
render: () => html`
25+
<div class="iati-select">
26+
<label for="iati-single-select" class="iati-select__label">
27+
Choose your language
28+
</label>
29+
<div class="iati-select__control-wrapper">
30+
<select id="iati-single-select" class="iati-select__control">
31+
<option>English</option>
32+
<option>French</option>
33+
</select>
34+
</div>
35+
</div>
36+
`,
37+
};
38+
39+
export const MultiSelect: Story = {
40+
name: "Multi Select",
41+
args: {
42+
label: "Choose your language(s)",
43+
options: ["English", "French"],
44+
},
45+
render: (args) => html`
46+
<multi-select .label=${args.label} .options=${args.options}></multi-select>
47+
`,
48+
};

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"compilerOptions": {
3+
"experimentalDecorators": true,
34
"jsx": "react"
45
}
56
}

0 commit comments

Comments
 (0)