Skip to content

Commit 355a124

Browse files
committed
feat(code-editor): add standard input props
such as `label`, `helperText`, `invalid`, `required`. This enables using the code-editor as a proper input field in `limel-form`
1 parent dece15d commit 355a124

File tree

4 files changed

+226
-1
lines changed

4 files changed

+226
-1
lines changed

etc/lime-elements.api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,15 @@ export namespace Components {
308308
"colorScheme": ColorScheme;
309309
"disabled": boolean;
310310
"fold": boolean;
311+
"helperText"?: string;
312+
"invalid": boolean;
313+
"label"?: string;
311314
"language": Language;
312315
"lineNumbers": boolean;
313316
"lineWrapping": boolean;
314317
"lint": boolean;
315318
"readonly": boolean;
319+
"required": boolean;
316320
"value": string;
317321
}
318322
export interface LimelCollapsibleSection {
@@ -1479,12 +1483,16 @@ export namespace JSX {
14791483
"colorScheme"?: ColorScheme;
14801484
"disabled"?: boolean;
14811485
"fold"?: boolean;
1486+
"helperText"?: string;
1487+
"invalid"?: boolean;
1488+
"label"?: string;
14821489
"language"?: Language;
14831490
"lineNumbers"?: boolean;
14841491
"lineWrapping"?: boolean;
14851492
"lint"?: boolean;
14861493
"onChange"?: (event: LimelCodeEditorCustomEvent<string>) => void;
14871494
"readonly"?: boolean;
1495+
"required"?: boolean;
14881496
"value"?: string;
14891497
}
14901498
export interface LimelCollapsibleSection {

src/components/code-editor/code-editor.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122

123123
:host {
124124
display: flex;
125+
flex-direction: column;
126+
width: 100%;
125127
font-size: var(--code-editor-font-size, 0.8125rem); // 13px
126128
@include light-mode-styles;
127129
}
@@ -136,6 +138,7 @@
136138
display: flex;
137139
align-items: stretch;
138140
width: 100%;
141+
min-height: 2.5rem; // Like `limel-input-field`
139142

140143
&.readonly {
141144
.CodeMirror-focused {
@@ -194,6 +197,10 @@
194197
}
195198
}
196199

200+
&-lines {
201+
padding: 0.25rem 0;
202+
}
203+
197204
&-line {
198205
}
199206

@@ -260,3 +267,9 @@
260267
opacity: 0.7;
261268
}
262269
}
270+
271+
limel-notched-outline {
272+
flex-grow: 1;
273+
}
274+
275+
@include mixins.hide-helper-line-when-not-needed(limel-code-editor);

src/components/code-editor/code-editor.tsx

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
EventEmitter,
88
State,
99
Watch,
10+
Host,
1011
} from '@stencil/core';
12+
import { createRandomString } from '../../util/random-string';
1113
import { ColorScheme, Language } from './code-editor.types';
1214
import CodeMirror from 'codemirror';
1315
import 'codemirror/mode/css/css';
@@ -28,6 +30,7 @@ import jslint from 'jsonlint-mod';
2830
* @exampleComponent limel-example-code-editor
2931
* @exampleComponent limel-example-code-editor-readonly-with-line-numbers
3032
* @exampleComponent limel-example-code-editor-fold-lint-wrap
33+
* @exampleComponent limel-example-code-editor-composite
3134
*/
3235
@Component({
3336
tag: 'limel-code-editor',
@@ -64,6 +67,31 @@ export class CodeEditor {
6467
@Prop({ reflect: true })
6568
public disabled = false;
6669

70+
/**
71+
* Set to `true` to indicate that the current value of the input editor is
72+
* invalid.
73+
*/
74+
@Prop({ reflect: true })
75+
public invalid = false;
76+
77+
/**
78+
* Set to `true` to indicate that the field is required.
79+
*/
80+
@Prop({ reflect: true })
81+
public required = false;
82+
83+
/**
84+
* The input label.
85+
*/
86+
@Prop({ reflect: true })
87+
public label?: string;
88+
89+
/**
90+
* Optional helper text to display below the input field when it has focus
91+
*/
92+
@Prop({ reflect: true })
93+
public helperText?: string;
94+
6795
/**
6896
* Displays line numbers in the editor
6997
*/
@@ -113,6 +141,13 @@ export class CodeEditor {
113141

114142
private editor: CodeMirror.Editor;
115143
private observer: ResizeObserver;
144+
private labelId: string;
145+
private helperTextId: string;
146+
147+
public constructor() {
148+
this.labelId = createRandomString();
149+
this.helperTextId = createRandomString();
150+
}
116151

117152
public connectedCallback() {
118153
this.observer = new ResizeObserver(this.handleResize) as any;
@@ -141,6 +176,7 @@ export class CodeEditor {
141176
}
142177

143178
this.editor = this.createEditor();
179+
this.updateInputFieldAccessibilityAttributes();
144180
}
145181

146182
@Watch('value')
@@ -171,6 +207,21 @@ export class CodeEditor {
171207
this.updateInputFieldAccessibilityAttributes();
172208
}
173209

210+
@Watch('invalid')
211+
protected watchInvalid() {
212+
this.updateInputFieldAccessibilityAttributes();
213+
}
214+
215+
@Watch('required')
216+
protected watchRequired() {
217+
this.updateInputFieldAccessibilityAttributes();
218+
}
219+
220+
@Watch('helperText')
221+
protected watchHelperText() {
222+
this.updateInputFieldAccessibilityAttributes();
223+
}
224+
174225
private handleChangeDarkMode = () => {
175226
if (this.colorScheme !== 'auto') {
176227
return;
@@ -282,9 +333,39 @@ export class CodeEditor {
282333
'is-light-mode': !this.isDarkMode(),
283334
};
284335

285-
return <div class={classList} />;
336+
return (
337+
<Host>
338+
<limel-notched-outline
339+
labelId={this.labelId}
340+
label={this.label}
341+
required={this.required}
342+
invalid={this.invalid}
343+
disabled={this.disabled}
344+
readonly={this.readonly}
345+
hasValue={!!this.value}
346+
hasFloatingLabel={true}
347+
>
348+
<div slot="content" class={classList} />
349+
</limel-notched-outline>
350+
{this.renderHelperLine()}
351+
</Host>
352+
);
286353
}
287354

355+
private renderHelperLine = () => {
356+
if (!this.helperText) {
357+
return;
358+
}
359+
360+
return (
361+
<limel-helper-line
362+
helperText={this.helperText}
363+
helperTextId={this.helperTextId}
364+
invalid={this.invalid}
365+
/>
366+
);
367+
};
368+
288369
private forceRedraw() {
289370
// eslint-disable-next-line sonarjs/pseudo-random
290371
this.random = Math.random();
@@ -320,6 +401,28 @@ export class CodeEditor {
320401
return;
321402
}
322403

404+
inputField.id = this.labelId;
405+
406+
if (this.helperText) {
407+
inputField.setAttribute('aria-describedby', this.helperTextId);
408+
inputField.setAttribute('aria-controls', this.helperTextId);
409+
} else {
410+
inputField.removeAttribute('aria-describedby');
411+
inputField.removeAttribute('aria-controls');
412+
}
413+
414+
if (this.required) {
415+
inputField.setAttribute('aria-required', 'true');
416+
} else {
417+
inputField.removeAttribute('aria-required');
418+
}
419+
420+
if (this.invalid) {
421+
inputField.setAttribute('aria-invalid', 'true');
422+
} else {
423+
inputField.removeAttribute('aria-invalid');
424+
}
425+
323426
if (this.disabled) {
324427
inputField.setAttribute('aria-disabled', 'true');
325428
} else {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Component, h, Host, State, Watch } from '@stencil/core';
2+
3+
/**
4+
* Composite Example
5+
*/
6+
@Component({
7+
tag: 'limel-example-code-editor-composite',
8+
shadow: true,
9+
})
10+
export class CodeEditorCompositeExample {
11+
@State()
12+
private required = false;
13+
14+
@State()
15+
private disabled = false;
16+
17+
@State()
18+
private readonly = false;
19+
20+
@State()
21+
private invalid = false;
22+
23+
@State()
24+
private value: string;
25+
26+
public render() {
27+
return (
28+
<Host>
29+
<limel-code-editor
30+
label="Write some good code"
31+
helperText={
32+
this.invalid
33+
? 'This code is invalid'
34+
: 'The code should be in JSON format'
35+
}
36+
value={this.value}
37+
required={this.required}
38+
invalid={this.invalid}
39+
disabled={this.disabled}
40+
readonly={this.readonly}
41+
onChange={this.handleChange}
42+
language="json"
43+
/>
44+
<limel-example-controls>
45+
<limel-switch
46+
value={this.disabled}
47+
label="disabled"
48+
onChange={this.setDisabled}
49+
/>
50+
<limel-switch
51+
value={this.readonly}
52+
label="readonly"
53+
onChange={this.setReadonly}
54+
/>
55+
<limel-switch
56+
value={this.required}
57+
label="required"
58+
onChange={this.setRequired}
59+
/>
60+
<limel-switch
61+
value={this.invalid}
62+
label="invalid"
63+
onChange={this.setInvalid}
64+
/>
65+
</limel-example-controls>
66+
,
67+
<limel-example-value value={this.value} />
68+
</Host>
69+
);
70+
}
71+
72+
@Watch('required')
73+
@Watch('value')
74+
protected checkValidity() {
75+
this.invalid = this.required && !this.value;
76+
}
77+
78+
private handleChange = (event: CustomEvent<string>) => {
79+
this.value = event.detail;
80+
};
81+
82+
private setDisabled = (event: CustomEvent<boolean>) => {
83+
event.stopPropagation();
84+
this.disabled = event.detail;
85+
};
86+
87+
private setReadonly = (event: CustomEvent<boolean>) => {
88+
event.stopPropagation();
89+
this.readonly = event.detail;
90+
};
91+
92+
private setRequired = (event: CustomEvent<boolean>) => {
93+
event.stopPropagation();
94+
this.required = event.detail;
95+
};
96+
97+
private setInvalid = (event: CustomEvent<boolean>) => {
98+
event.stopPropagation();
99+
this.invalid = event.detail;
100+
};
101+
}

0 commit comments

Comments
 (0)