Skip to content

Commit d5f378c

Browse files
Merge pull request #55 from umbraco/feature/textarea
Feature/textarea
2 parents e9e1a63 + 9153a60 commit d5f378c

File tree

12 files changed

+7535
-13481
lines changed

12 files changed

+7535
-13481
lines changed

package-lock.json

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

packages/uui-caret/tsconfig.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,11 @@
77
"rootDir": "./lib",
88
"composite": true
99
},
10-
"include": [
11-
"./**/*.ts"
12-
],
13-
"exclude": [
14-
"./**/*.test.ts",
15-
"./**/*.story.ts"
16-
],
10+
"include": ["./**/*.ts"],
11+
"exclude": ["./**/*.test.ts", "./**/*.story.ts"],
1712
"references": [
1813
{
1914
"path": "../uui-base"
2015
}
2116
]
22-
}
17+
}

packages/uui-menu-item/tsconfig.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,11 @@
77
"rootDir": "./lib",
88
"composite": true
99
},
10-
"include": [
11-
"./**/*.ts"
12-
],
13-
"exclude": [
14-
"./**/*.test.ts",
15-
"./**/*.story.ts"
16-
],
10+
"include": ["./**/*.ts"],
11+
"exclude": ["./**/*.test.ts", "./**/*.story.ts"],
1712
"references": [
1813
{
1914
"path": "../uui-base"
2015
}
2116
]
22-
}
17+
}

packages/uui-textarea/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# uui-textarea
2+
3+
![npm](https://img.shields.io/npm/v/@umbraco-ui/uui-textarea?logoColor=%231B264F)
4+
5+
Umbraco style textarea component.
6+
7+
## Installation
8+
9+
### ES imports
10+
11+
```zsh
12+
npm i @umbraco-ui/uui-textarea
13+
```
14+
15+
Import the registration of `<uui-textarea>` via:
16+
17+
```javascript
18+
import '@umbraco-ui/uui-textarea/lib';
19+
```
20+
21+
When looking to leverage the `UUITextareaElement` base class as a type and/or for extension purposes, do so via:
22+
23+
```javascript
24+
import { UUITextareaElement } from '@umbraco-ui/uui-textarea/lib/uui-textarea.element';
25+
```
26+
27+
### CDN
28+
29+
The component is available via CDN. This means it can be added to your application without the need of any bundler configuration. Here is how to use it with jsDelivr.
30+
31+
```html
32+
<!-- Latest Version -->
33+
<script src="https://cdn.jsdelivr.net/npm/@umbraco-ui/uui-textarea@latest/dist/uui-textarea.min.js"></script>
34+
35+
<!-- Specific version -->
36+
<script src="https://cdn.jsdelivr.net/npm/@umbraco-ui/[email protected]/dist/uui-textarea.min.js"></script>
37+
```
38+
39+
## Usage
40+
41+
```html
42+
<uui-textarea></uui-textarea>
43+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { UUIEvent } from '@umbraco-ui/uui-base/lib/events';
2+
import { UUITextareaElement } from './uui-textarea.element';
3+
4+
export class UUITextareaEvent extends UUIEvent<{}, UUITextareaElement> {
5+
public static readonly CHANGE: string = 'change';
6+
public static readonly INPUT: string = 'input';
7+
}

packages/uui-textarea/lib/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { UUITextareaElement } from './uui-textarea.element';
2+
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
3+
4+
defineElement('uui-textarea', UUITextareaElement as any);
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import { LitElement, html, css } from 'lit';
2+
import { property, state, query } from 'lit/decorators.js';
3+
import { LabelMixin } from '@umbraco-ui/uui-base/lib/mixins';
4+
import { UUITextareaEvent } from './UUITextareaEvent';
5+
import { ifDefined } from 'lit/directives/if-defined.js';
6+
/**
7+
* @element uui-textarea
8+
* @extends LabelMixin(LitElement)
9+
* @slot textarea label - for the textarea label text.
10+
* @fires UUITextareaEvent#change on change
11+
* @fires InputEvent#input on input
12+
* @fires KeyboardEvent#keyup on keyup
13+
* @cssprop --uui-textarea-min-height - Sets the minimum height of the textarea
14+
* @cssprop --uui-textarea-max-height - Sets the maximum height of the textarea
15+
*/
16+
export class UUITextareaElement extends LabelMixin(
17+
'textarea label',
18+
LitElement
19+
) {
20+
static styles = [
21+
css`
22+
:host([disabled]) .label,
23+
:host([disabled]) #max-length-counter {
24+
color: var(--uui-interface-contrast-disabled);
25+
}
26+
:host([error]) textarea {
27+
border: 1px solid var(--uui-look-danger-border) !important;
28+
}
29+
:host([error]) textarea[disabled] {
30+
border: 1px solid var(--uui-look-danger-border) !important;
31+
}
32+
:host([auto-height]) textarea {
33+
resize: none;
34+
}
35+
.label {
36+
display: inline-block;
37+
margin-bottom: var(--uui-size-1);
38+
font-weight: bold;
39+
}
40+
textarea[disabled] {
41+
cursor: not-allowed;
42+
background-color: var(
43+
--uui-textarea-background-color-disabled,
44+
var(--uui-interface-surface-disabled)
45+
);
46+
border: 1px solid
47+
var(
48+
--uui-textarea-border-color-disabled,
49+
var(--uui-interface-border-disable)
50+
);
51+
52+
color: var(--uui-interface-contrast-disabled);
53+
}
54+
textarea {
55+
font-family: inherit;
56+
box-sizing: border-box;
57+
min-width: 100%;
58+
max-width: 100%;
59+
font-size: var(--uui-size-5);
60+
padding: var(--uui-size-2);
61+
border: 1px solid
62+
var(--uui-textarea-border-color, var(--uui-interface-border));
63+
border-radius: 0;
64+
outline: none;
65+
min-height: var(--uui-textarea-min-height);
66+
max-height: var(--uui-textarea-max-height);
67+
}
68+
#lengths-container {
69+
display: flex;
70+
}
71+
#min-length-counter {
72+
color: var(--uui-look-danger-surface);
73+
margin-right: 1em;
74+
}
75+
#max-length-counter {
76+
display: inline-block;
77+
width: min-content;
78+
}
79+
#max-length-counter.maxed {
80+
animation-name: maxed;
81+
animation-duration: 0.1s;
82+
animation-direction: alternate;
83+
animation-iteration-count: 2;
84+
}
85+
@keyframes maxed {
86+
from {
87+
transform: scale(1);
88+
}
89+
to {
90+
transform: scale(1.1);
91+
}
92+
}
93+
`,
94+
];
95+
96+
/**
97+
* This is a static class field indicating that the element is can be used inside a native form and participate in its events. It may require a polyfill, check support here https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals. Read more about form controls here https://web.dev/more-capable-form-controls/
98+
* @type {boolean}
99+
*/
100+
static readonly formAssociated = true;
101+
102+
private _internals;
103+
104+
constructor() {
105+
super();
106+
this._internals = (this as any).attachInternals();
107+
}
108+
109+
/**
110+
* Defines the textarea placeholder.
111+
* @type {string}
112+
* @attr
113+
* @default ''
114+
*/
115+
@property()
116+
placeholder = '';
117+
118+
/**
119+
* Disables the textarea.
120+
* @type {boolean}
121+
* @attr
122+
* @default false
123+
*/
124+
@property({ type: Boolean, reflect: true })
125+
disabled = false;
126+
127+
/**
128+
* Set to true to hide the labeling provided by the component.
129+
* @type {boolean}
130+
* @attr hide-label
131+
* @default false
132+
*/
133+
@property({ type: Boolean, attribute: 'hide-label', reflect: true })
134+
hideLabel = false;
135+
136+
@state()
137+
private _value = '';
138+
139+
/**
140+
* This is a value property of the uui-textarea.
141+
* @type {string}
142+
* @attr
143+
* @default ''
144+
*/
145+
@property()
146+
get value() {
147+
return this._value;
148+
}
149+
set value(newValue) {
150+
this._value = newValue;
151+
152+
if (
153+
'ElementInternals' in window &&
154+
//@ts-ignore
155+
'setFormValue' in window.ElementInternals.prototype
156+
) {
157+
this._internals.setFormValue(this._value);
158+
}
159+
}
160+
161+
/**
162+
* This is a name property of the `<uui-textarea>` component. It reflects the behaviour of the native `<textarea>` element and its name attribute.
163+
* @type {string}
164+
* @attr
165+
* @default ''
166+
*/
167+
@property({ type: String })
168+
name = '';
169+
170+
/**
171+
* Set to true if the component should have an error state. Property is reflected to the corresponding attribute.
172+
* @type {boolean}
173+
* @attr
174+
* @default false
175+
*/
176+
@property({ type: Boolean, reflect: true })
177+
error = false;
178+
179+
@query('#textarea')
180+
protected _textarea!: HTMLInputElement;
181+
182+
/**
183+
* Defines the min length of the textarea.
184+
* @type {number}
185+
* @attr
186+
* @default undefined
187+
*/
188+
@property({ type: Number })
189+
minLength = 0;
190+
191+
/**
192+
* Defines the max length of the textarea.
193+
* @type {number}
194+
* @attr
195+
* @default undefined
196+
*/
197+
@property({ type: Number })
198+
maxLength = 0;
199+
200+
/**
201+
* Enables automatic height adjustment. The height will be confined within the min and max height if defined.
202+
* @type {boolean}
203+
* @attr
204+
* @default false
205+
*/
206+
@property({ type: Boolean, attribute: 'auto-height', reflect: true })
207+
autoHeight = false;
208+
209+
private onInput(e: Event) {
210+
this.value = (e.target as HTMLInputElement).value;
211+
this.dispatchEvent(
212+
new UUITextareaEvent(UUITextareaEvent.INPUT, { bubbles: true })
213+
);
214+
215+
if (this.autoHeight) {
216+
this.autoUpdateHeight();
217+
}
218+
}
219+
220+
private onChange() {
221+
this.dispatchEvent(
222+
new UUITextareaEvent(UUITextareaEvent.CHANGE, { bubbles: true })
223+
);
224+
}
225+
226+
private autoUpdateHeight() {
227+
const host = this.shadowRoot!.host! as HTMLElement;
228+
const input = this._textarea;
229+
230+
// Temporarily lock the height of the shadowroot host to prevent
231+
// the page scroll from moving when changing the textarea height
232+
const scrollTop = host.scrollTop;
233+
const hostHeight = getComputedStyle(host).height;
234+
host.style.display = 'block';
235+
host.style.height = hostHeight;
236+
237+
input.style.height = 'auto';
238+
239+
if (input.scrollHeight > input.clientHeight) {
240+
input.style.height = input.scrollHeight + 'px';
241+
}
242+
243+
// Reset host styles and scroll to where we were
244+
host.style.removeProperty('display');
245+
host.style.removeProperty('height');
246+
host.scrollTop = scrollTop;
247+
}
248+
249+
renderMaxLength() {
250+
return html`<span
251+
id="max-length-counter"
252+
class=${this.value.length >= this.maxLength! ? 'maxed' : ''}
253+
>${this.value ? this.value.length : 0}/${this.maxLength}</span
254+
>`;
255+
}
256+
257+
renderMinLength() {
258+
const shouldRender = this.minLength - this.value.length > 0;
259+
return shouldRender
260+
? html`<span id="min-length-counter">
261+
${this.minLength - this.value.length}
262+
</span>`
263+
: '';
264+
}
265+
266+
render() {
267+
return html`
268+
${this.hideLabel === false ? this.renderLabel() : ''}
269+
<textarea
270+
maxlength=${ifDefined(this.maxLength > 0 ? this.maxLength : undefined)}
271+
minlength=${this.minLength}
272+
id="textarea"
273+
.value=${this.value}
274+
.name=${this.name}
275+
placeholder=${this.placeholder}
276+
aria-label=${this.label}
277+
.disabled=${this.disabled}
278+
@input=${this.onInput}
279+
@change=${this.onChange}>
280+
</textarea>
281+
<div id="lengths-container">
282+
${this.minLength > 0 ? this.renderMinLength() : ''}
283+
${this.maxLength > 0 ? this.renderMaxLength() : ''}
284+
</div>
285+
`;
286+
}
287+
}

0 commit comments

Comments
 (0)