mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
Refactor label + help text logic
This commit is contained in:
@@ -101,63 +101,21 @@ Use the `prefix` and `suffix` slots to add icons.
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the input an accessible label.
|
||||
Use the `label` attribute to give the input an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-input label="Name"></sl-input>
|
||||
<br>
|
||||
<sl-input type="email" label="Email" placeholder="bob@example.com"></sl-input>
|
||||
<sl-input label="What is your name?"></sl-input>
|
||||
```
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to an input with the `help-text` slot.
|
||||
Add descriptive help text to an input with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-input label="Nickname">
|
||||
<div slot="help-text">What would you like people to call you?</div>
|
||||
</sl-input>
|
||||
```
|
||||
|
||||
### Inputs with Dropdowns
|
||||
|
||||
Dropdowns can be used in the `prefix` or `suffix` slot to make inputs more versatile. Make sure to use the `hoist` prop so the dropdown breaks out of the input's overflow.
|
||||
|
||||
```html preview
|
||||
<div class="input-dropdowns">
|
||||
<sl-input type="tel" label="Phone">
|
||||
<sl-icon slot="prefix" name="telephone"></sl-icon>
|
||||
<sl-dropdown slot="suffix" placement="bottom-end" hoist>
|
||||
<sl-button slot="trigger" caret type="text" size="small">Home</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item checked>Home</sl-menu-item>
|
||||
<sl-menu-item>Mobile</sl-menu-item>
|
||||
<sl-menu-item>Work</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
<div slot="help-text">
|
||||
Please enter a phone number where we can reach you.
|
||||
</div>
|
||||
</sl-input>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.input-dropdowns');
|
||||
const dropdown = container.querySelector('sl-dropdown');
|
||||
const trigger = dropdown.querySelector('sl-button');
|
||||
|
||||
dropdown.addEventListener('sl-select', event => {
|
||||
const selectedItem = event.detail.item;
|
||||
trigger.textContent = selectedItem.textContent;
|
||||
[...dropdown.querySelectorAll('sl-menu-item')].map(item => item.checked = item === selectedItem);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.input-dropdowns sl-dropdown {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
</style>
|
||||
<sl-input
|
||||
label="Nickname"
|
||||
help-text="What would you like people to call you?"
|
||||
></sl-input>
|
||||
```
|
||||
|
||||
[component-metadata:sl-input]
|
||||
|
||||
@@ -163,7 +163,7 @@ The `value` prop is bound to the current selection. As the selection changes, so
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the select an accessible label.
|
||||
Use the `label` attribute to give the select an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
@@ -175,15 +175,16 @@ Use the `label` attribute to give the select an accessible label.
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to an input with the `help-text` slot.
|
||||
Add descriptive help text to a select with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Experience">
|
||||
<sl-select
|
||||
label="Experience"
|
||||
help-text="Please tell us your skill level."
|
||||
>
|
||||
<sl-menu-item value="option-1">Novice</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Intermediate</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Advanced</sl-menu-item>
|
||||
|
||||
<div slot="help-text">Please tell us your skill level.</div>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ Use the `size` attribute to change a textarea's size.
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the textarea an accessible label.
|
||||
Use the `label` attribute to give the textarea an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-textarea label="Comments"></sl-textarea>
|
||||
@@ -60,11 +60,13 @@ Use the `label` attribute to give the textarea an accessible label.
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to a textarea with the `help-text` slot.
|
||||
Add descriptive help text to a textarea with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-textarea label="Feedback">
|
||||
<div slot="help-text">Please tell us what you think.</div>
|
||||
<sl-textarea
|
||||
label="Feedback"
|
||||
help-text="Please tell us what you think."
|
||||
>
|
||||
</sl-textarea>
|
||||
```
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
||||
- 🚨 BREAKING CHANGE: Removed `copy-button` part from `sl-color-picker` since copying is now done by clicking the preview
|
||||
- Added `getFormattedValue()` method to `sl-color-picker` so you can retrieve the current value in any format
|
||||
- Added visual separators between solid buttons in `sl-button-group`
|
||||
- Added `help-text` prop to `sl-input`, `sl-textarea`, and `sl-select`
|
||||
- Fixed a bug where moving the mouse while `sl-dropdown` is closing would remove focus from the trigger
|
||||
- Fixed a bug where `sl-menu-item` didn't set a default color in the dark theme
|
||||
- Fixed a bug where `sl-color-picker` preview wouldn't update in Safari
|
||||
@@ -31,6 +32,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
||||
- Improved accessibility in `sl-tooltip` by allowing escape to dismiss it [#219](https://github.com/shoelace-style/shoelace/issues/219)
|
||||
- Improved slot detection in `sl-card`, `sl-dialog`, and `sl-drawer`
|
||||
- Made `@types/resize-observer-browser` a dependency so users don't have to install it manually
|
||||
- Refactored internal label + help text logic into a functional component used by `sl-input`, `sl-textarea`, and `sl-select`
|
||||
- Removed `sl-blur` and `sl-focus` events from `sl-menu` since menus can't have focus as of 2.0.0-beta.22
|
||||
- Updated `sl-spinner` so the indicator is more obvious
|
||||
- Updated to Bootstrap Icons 1.2.1
|
||||
|
||||
36
src/components.d.ts
vendored
36
src/components.d.ts
vendored
@@ -676,6 +676,10 @@ export namespace Components {
|
||||
* Set to true to disable the input.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
* The input's help text. Alternatively, you can use the help-text slot.
|
||||
*/
|
||||
"helpText": string;
|
||||
/**
|
||||
* The input's inputmode attribute.
|
||||
*/
|
||||
@@ -685,7 +689,7 @@ export namespace Components {
|
||||
*/
|
||||
"invalid": boolean;
|
||||
/**
|
||||
* The input's label.
|
||||
* The input's label. Alternatively, you can use the label slot.
|
||||
*/
|
||||
"label": string;
|
||||
/**
|
||||
@@ -998,6 +1002,10 @@ export namespace Components {
|
||||
* Set to true to disable the select control.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
* The select's help text. Alternatively, you can use the help-text slot.
|
||||
*/
|
||||
"helpText": string;
|
||||
/**
|
||||
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with `overflow: auto|scroll`.
|
||||
*/
|
||||
@@ -1007,7 +1015,7 @@ export namespace Components {
|
||||
*/
|
||||
"invalid": boolean;
|
||||
/**
|
||||
* The select's label.
|
||||
* The select's label. Alternatively, you can use the label slot.
|
||||
*/
|
||||
"label": string;
|
||||
/**
|
||||
@@ -1190,6 +1198,10 @@ export namespace Components {
|
||||
* Set to true to disable the textarea.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
* The textarea's help text. Alternatively, you can use the help-text slot.
|
||||
*/
|
||||
"helpText": string;
|
||||
/**
|
||||
* The textarea's inputmode attribute.
|
||||
*/
|
||||
@@ -1199,7 +1211,7 @@ export namespace Components {
|
||||
*/
|
||||
"invalid": boolean;
|
||||
/**
|
||||
* The textarea's label.
|
||||
* The textarea's label. Alternatively, you can use the label slot.
|
||||
*/
|
||||
"label": string;
|
||||
/**
|
||||
@@ -2374,6 +2386,10 @@ declare namespace LocalJSX {
|
||||
* Set to true to disable the input.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* The input's help text. Alternatively, you can use the help-text slot.
|
||||
*/
|
||||
"helpText"?: string;
|
||||
/**
|
||||
* The input's inputmode attribute.
|
||||
*/
|
||||
@@ -2383,7 +2399,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"invalid"?: boolean;
|
||||
/**
|
||||
* The input's label.
|
||||
* The input's label. Alternatively, you can use the label slot.
|
||||
*/
|
||||
"label"?: string;
|
||||
/**
|
||||
@@ -2676,6 +2692,10 @@ declare namespace LocalJSX {
|
||||
* Set to true to disable the select control.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* The select's help text. Alternatively, you can use the help-text slot.
|
||||
*/
|
||||
"helpText"?: string;
|
||||
/**
|
||||
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with `overflow: auto|scroll`.
|
||||
*/
|
||||
@@ -2685,7 +2705,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"invalid"?: boolean;
|
||||
/**
|
||||
* The select's label.
|
||||
* The select's label. Alternatively, you can use the label slot.
|
||||
*/
|
||||
"label"?: string;
|
||||
/**
|
||||
@@ -2872,6 +2892,10 @@ declare namespace LocalJSX {
|
||||
* Set to true to disable the textarea.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* The textarea's help text. Alternatively, you can use the help-text slot.
|
||||
*/
|
||||
"helpText"?: string;
|
||||
/**
|
||||
* The textarea's inputmode attribute.
|
||||
*/
|
||||
@@ -2881,7 +2905,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"invalid"?: boolean;
|
||||
/**
|
||||
* The textarea's label.
|
||||
* The textarea's label. Alternatively, you can use the label slot.
|
||||
*/
|
||||
"label"?: string;
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@import 'component';
|
||||
@import 'form-control-label';
|
||||
@import 'form-control-help-text';
|
||||
@import '../../functional-components/form-control/form-control';
|
||||
|
||||
/**
|
||||
* @prop --focus-ring: The focus ring style to use when the control receives focus, a `box-shadow` property.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import FormControl from '../../functional-components/form-control/form-control';
|
||||
import { hasSlot } from '../../utilities/slot';
|
||||
|
||||
let id = 0;
|
||||
@@ -13,10 +14,10 @@ let id = 0;
|
||||
* @slot clear-icon - An icon to use in lieu of the default clear icon.
|
||||
* @slot show-password-icon - An icon to use in lieu of the default show password icon.
|
||||
* @slot hide-password-icon - An icon to use in lieu of the default hide password icon.
|
||||
* @slot help-text - Help text that describes how to use the input.
|
||||
* @slot help-text - Help text that describes how to use the input. Alternatively, you can use the help-text prop.
|
||||
*
|
||||
* @part base - The component's base wrapper.
|
||||
* @part form-control - The form control that wraps the label and the input.
|
||||
* @part form-control - The form control that wraps the label, input, and help-text.
|
||||
* @part label - The input label.
|
||||
* @part input - The input control.
|
||||
* @part prefix - The input prefix container.
|
||||
@@ -40,7 +41,8 @@ export class Input {
|
||||
@Element() host: HTMLSlInputElement;
|
||||
|
||||
@State() hasFocus = false;
|
||||
@State() hasLabel = false;
|
||||
@State() hasHelpTextSlot = false;
|
||||
@State() hasLabelSlot = false;
|
||||
@State() isPasswordVisible = false;
|
||||
|
||||
/** The input's type. */
|
||||
@@ -58,9 +60,12 @@ export class Input {
|
||||
/** Set to true to draw a pill-style input with rounded edges. */
|
||||
@Prop({ reflect: true }) pill = false;
|
||||
|
||||
/** The input's label. */
|
||||
/** The input's label. Alternatively, you can use the label slot. */
|
||||
@Prop() label = '';
|
||||
|
||||
/** The input's help text. Alternatively, you can use the help-text slot. */
|
||||
@Prop() helpText = '';
|
||||
|
||||
/** The input's placeholder text. */
|
||||
@Prop() placeholder: string;
|
||||
|
||||
@@ -123,7 +128,7 @@ export class Input {
|
||||
|
||||
@Watch('label')
|
||||
handleLabelChange() {
|
||||
this.detectLabel();
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
@Watch('value')
|
||||
@@ -147,7 +152,6 @@ export class Input {
|
||||
@Event({ eventName: 'sl-blur' }) slBlur: EventEmitter;
|
||||
|
||||
connectedCallback() {
|
||||
this.detectLabel = this.detectLabel.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.handleInvalid = this.handleInvalid.bind(this);
|
||||
@@ -155,10 +159,17 @@ export class Input {
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleClearClick = this.handleClearClick.bind(this);
|
||||
this.handlePasswordToggle = this.handlePasswordToggle.bind(this);
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
|
||||
this.host.shadowRoot.addEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
this.detectLabel();
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.host.shadowRoot.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
/** Sets focus on the input. */
|
||||
@@ -219,10 +230,6 @@ export class Input {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
detectLabel() {
|
||||
this.hasLabel = this.label.length > 0 || hasSlot(this.host, 'label');
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
this.value = this.input.value;
|
||||
this.slChange.emit();
|
||||
@@ -261,32 +268,23 @@ export class Input {
|
||||
this.isPasswordVisible = !this.isPasswordVisible;
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasHelpTextSlot = hasSlot(this.host, 'help-text');
|
||||
this.hasLabelSlot = hasSlot(this.host, 'label');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
part="form-control"
|
||||
class={{
|
||||
'form-control': true,
|
||||
'form-control--has-label': this.hasLabel,
|
||||
'form-control--invalid': this.invalid
|
||||
}}
|
||||
<FormControl
|
||||
inputId={this.inputId}
|
||||
label={this.label}
|
||||
labelId={this.labelId}
|
||||
hasLabelSlot={this.hasLabelSlot}
|
||||
helpTextId={this.helpTextId}
|
||||
helpText={this.helpText}
|
||||
hasHelpTextSlot={this.hasHelpTextSlot}
|
||||
size={this.size}
|
||||
>
|
||||
<label
|
||||
part="label"
|
||||
class={{
|
||||
label: true,
|
||||
'label--small': this.size === 'small',
|
||||
'label--medium': this.size === 'medium',
|
||||
'label--large': this.size === 'large',
|
||||
'label--invalid': this.invalid
|
||||
}}
|
||||
htmlFor={this.inputId}
|
||||
>
|
||||
<slot name="label" onSlotchange={this.detectLabel}>
|
||||
{this.label}
|
||||
</slot>
|
||||
</label>
|
||||
|
||||
<div
|
||||
part="base"
|
||||
class={{
|
||||
@@ -382,21 +380,7 @@ export class Input {
|
||||
<slot name="suffix" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
part="help-text"
|
||||
id={this.helpTextId}
|
||||
class={{
|
||||
'help-text': true,
|
||||
'help-text--small': this.size === 'small',
|
||||
'help-text--medium': this.size === 'medium',
|
||||
'help-text--large': this.size === 'large',
|
||||
'help-text--invalid': this.invalid
|
||||
}}
|
||||
>
|
||||
<slot name="help-text" />
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@import 'component';
|
||||
@import 'form-control-label';
|
||||
@import 'form-control-help-text';
|
||||
@import '../../functional-components/form-control/form-control';
|
||||
@import 'mixins/hidden';
|
||||
@import 'mixins/hide-scrollbar';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import FormControl from '../../functional-components/form-control/form-control';
|
||||
import { getTextContent } from '../../utilities/slot';
|
||||
import { hasSlot } from '../../utilities/slot';
|
||||
|
||||
@@ -14,7 +15,7 @@ let id = 0;
|
||||
*
|
||||
* @part base - The component's base wrapper.
|
||||
* @part clear-button - The input's clear button, exported from <sl-input>.
|
||||
* @part form-control - The form control that wraps the label and the input.
|
||||
* @part form-control - The form control that wraps the label, input, and help text.
|
||||
* @part help-text - The select's help text.
|
||||
* @part icon - The select's icon.
|
||||
* @part label - The select's label.
|
||||
@@ -41,7 +42,8 @@ export class Select {
|
||||
@Element() host: HTMLSlSelectElement;
|
||||
|
||||
@State() hasFocus = false;
|
||||
@State() hasLabel = false;
|
||||
@State() hasHelpTextSlot = false;
|
||||
@State() hasLabelSlot = false;
|
||||
@State() isOpen = false;
|
||||
@State() items = [];
|
||||
@State() displayLabel = '';
|
||||
@@ -80,9 +82,12 @@ export class Select {
|
||||
/** Set to true to draw a pill-style select with rounded edges. */
|
||||
@Prop() pill = false;
|
||||
|
||||
/** The select's label. */
|
||||
/** The select's label. Alternatively, you can use the label slot. */
|
||||
@Prop() label = '';
|
||||
|
||||
/** The select's help text. Alternatively, you can use the help-text slot. */
|
||||
@Prop() helpText = '';
|
||||
|
||||
/** The select's required attribute. */
|
||||
@Prop() required = false;
|
||||
|
||||
@@ -101,7 +106,7 @@ export class Select {
|
||||
|
||||
@Watch('label')
|
||||
handleLabelChange() {
|
||||
this.detectLabel();
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
@Watch('multiple')
|
||||
@@ -128,7 +133,6 @@ export class Select {
|
||||
@Event({ eventName: 'sl-blur' }) slBlur: EventEmitter;
|
||||
|
||||
connectedCallback() {
|
||||
this.detectLabel = this.detectLabel.bind(this);
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleClearClick = this.handleClearClick.bind(this);
|
||||
@@ -139,10 +143,12 @@ export class Select {
|
||||
this.handleMenuSelect = this.handleMenuSelect.bind(this);
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
this.handleTagInteraction = this.handleTagInteraction.bind(this);
|
||||
|
||||
this.host.shadowRoot.addEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
this.detectLabel();
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
@@ -153,6 +159,10 @@ export class Select {
|
||||
requestAnimationFrame(() => this.syncItemsFromValue());
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.host.shadowRoot.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
@Method()
|
||||
async reportValidity() {
|
||||
@@ -166,10 +176,6 @@ export class Select {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
detectLabel() {
|
||||
this.hasLabel = this.label.length > 0 || hasSlot(this.host, 'label');
|
||||
}
|
||||
|
||||
getItemLabel(item: HTMLSlMenuItemElement) {
|
||||
const slot = item.shadowRoot.querySelector('slot:not([name])') as HTMLSlotElement;
|
||||
return getTextContent(slot);
|
||||
@@ -283,6 +289,8 @@ export class Select {
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasHelpTextSlot = hasSlot(this.host, 'help-text');
|
||||
this.hasLabelSlot = hasSlot(this.host, 'label');
|
||||
this.syncItemsFromValue();
|
||||
this.reportDuplicateItemValues();
|
||||
}
|
||||
@@ -384,32 +392,17 @@ export class Select {
|
||||
const hasSelection = this.multiple ? this.value.length > 0 : this.value !== '';
|
||||
|
||||
return (
|
||||
<div
|
||||
part="form-control"
|
||||
class={{
|
||||
'form-control': true,
|
||||
'form-control--has-label': this.hasLabel,
|
||||
'form-control--invalid': this.invalid
|
||||
}}
|
||||
<FormControl
|
||||
inputId={this.inputId}
|
||||
label={this.label}
|
||||
labelId={this.labelId}
|
||||
hasLabelSlot={this.hasLabelSlot}
|
||||
helpTextId={this.helpTextId}
|
||||
helpText={this.helpText}
|
||||
hasHelpTextSlot={this.hasHelpTextSlot}
|
||||
size={this.size}
|
||||
onLabelClick={this.handleLabelClick}
|
||||
>
|
||||
<label
|
||||
part="label"
|
||||
id={this.labelId}
|
||||
class={{
|
||||
label: true,
|
||||
'label--small': this.size === 'small',
|
||||
'label--medium': this.size === 'medium',
|
||||
'label--large': this.size === 'large',
|
||||
'label--invalid': this.invalid
|
||||
}}
|
||||
htmlFor={this.inputId}
|
||||
onClick={this.handleLabelClick}
|
||||
>
|
||||
<slot name="label" onSlotchange={this.detectLabel}>
|
||||
{this.label}
|
||||
</slot>
|
||||
</label>
|
||||
|
||||
<sl-dropdown
|
||||
part="base"
|
||||
ref={el => (this.dropdown = el)}
|
||||
@@ -492,21 +485,7 @@ export class Select {
|
||||
<slot onSlotchange={this.handleSlotChange} />
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
|
||||
<div
|
||||
part="help-text"
|
||||
id={this.helpTextId}
|
||||
class={{
|
||||
'help-text': true,
|
||||
'help-text--small': this.size === 'small',
|
||||
'help-text--medium': this.size === 'medium',
|
||||
'help-text--large': this.size === 'large',
|
||||
'help-text--invalid': this.invalid
|
||||
}}
|
||||
>
|
||||
<slot name="help-text" />
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@import 'component';
|
||||
@import 'form-control-label';
|
||||
@import 'form-control-help-text';
|
||||
@import '../../functional-components/form-control/form-control';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import FormControl from '../../functional-components/form-control/form-control';
|
||||
import { hasSlot } from '../../utilities/slot';
|
||||
|
||||
let id = 0;
|
||||
@@ -11,7 +12,7 @@ let id = 0;
|
||||
* @slot help-text - Help text that describes how to use the input.
|
||||
*
|
||||
* @part base - The component's base wrapper.
|
||||
* @part form-control - The form control that wraps the textarea and label.
|
||||
* @part form-control - The form control that wraps the label, textarea, and help text.
|
||||
* @part label - The textarea label.
|
||||
* @part textarea - The textarea control.
|
||||
* @part help-text - The textarea help text.
|
||||
@@ -23,7 +24,7 @@ let id = 0;
|
||||
shadow: true
|
||||
})
|
||||
export class Textarea {
|
||||
textareaId = `textarea-${++id}`;
|
||||
inputId = `textarea-${++id}`;
|
||||
labelId = `textarea-label-${id}`;
|
||||
helpTextId = `textarea-help-text-${id}`;
|
||||
resizeObserver: ResizeObserver;
|
||||
@@ -32,7 +33,8 @@ export class Textarea {
|
||||
@Element() host: HTMLSlTextareaElement;
|
||||
|
||||
@State() hasFocus = false;
|
||||
@State() hasLabel = false;
|
||||
@State() hasHelpTextSlot = false;
|
||||
@State() hasLabelSlot = false;
|
||||
|
||||
/** The textarea's size. */
|
||||
@Prop({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
@@ -43,9 +45,12 @@ export class Textarea {
|
||||
/** The textarea's value attribute. */
|
||||
@Prop({ mutable: true, reflect: true }) value = '';
|
||||
|
||||
/** The textarea's label. */
|
||||
/** The textarea's label. Alternatively, you can use the label slot. */
|
||||
@Prop() label = '';
|
||||
|
||||
/** The textarea's help text. Alternatively, you can use the help-text slot. */
|
||||
@Prop() helpText = '';
|
||||
|
||||
/** The textarea's placeholder text. */
|
||||
@Prop() placeholder: string;
|
||||
|
||||
@@ -108,7 +113,7 @@ export class Textarea {
|
||||
|
||||
@Watch('label')
|
||||
handleLabelChange() {
|
||||
this.detectLabel();
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
@Watch('rows')
|
||||
@@ -122,15 +127,17 @@ export class Textarea {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.detectLabel = this.detectLabel.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
|
||||
this.host.shadowRoot.addEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
this.detectLabel();
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
@@ -141,6 +148,7 @@ export class Textarea {
|
||||
|
||||
disconnectedCallback() {
|
||||
this.resizeObserver.unobserve(this.textarea);
|
||||
this.host.shadowRoot.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
/** Sets focus on the textarea. */
|
||||
@@ -202,10 +210,6 @@ export class Textarea {
|
||||
this.invalid = !this.textarea.checkValidity();
|
||||
}
|
||||
|
||||
detectLabel() {
|
||||
this.hasLabel = this.label.length > 0 || hasSlot(this.host, 'label');
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
this.slChange.emit();
|
||||
}
|
||||
@@ -226,6 +230,11 @@ export class Textarea {
|
||||
this.slFocus.emit();
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasLabelSlot = hasSlot(this.host, 'label');
|
||||
this.hasHelpTextSlot = hasSlot(this.host, 'help-text');
|
||||
}
|
||||
|
||||
setTextareaHeight() {
|
||||
if (this.resize === 'auto') {
|
||||
this.textarea.style.height = 'auto';
|
||||
@@ -237,29 +246,16 @@ export class Textarea {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
part="form-control"
|
||||
class={{
|
||||
'form-control': true,
|
||||
'form-control--has-label': this.hasLabel,
|
||||
'form-control--invalid': this.invalid
|
||||
}}
|
||||
<FormControl
|
||||
inputId={this.inputId}
|
||||
label={this.label}
|
||||
labelId={this.labelId}
|
||||
hasLabelSlot={this.hasLabelSlot}
|
||||
helpTextId={this.helpTextId}
|
||||
helpText={this.helpText}
|
||||
hasHelpTextSlot={this.hasHelpTextSlot}
|
||||
size={this.size}
|
||||
>
|
||||
<label
|
||||
part="label"
|
||||
class={{
|
||||
label: true,
|
||||
'label--small': this.size === 'small',
|
||||
'label--medium': this.size === 'medium',
|
||||
'label--large': this.size === 'large',
|
||||
'label--invalid': this.invalid
|
||||
}}
|
||||
htmlFor={this.textareaId}
|
||||
>
|
||||
<slot name="label" onSlotchange={this.detectLabel}>
|
||||
{this.label}
|
||||
</slot>
|
||||
</label>
|
||||
<div
|
||||
part="base"
|
||||
class={{
|
||||
@@ -285,7 +281,7 @@ export class Textarea {
|
||||
<textarea
|
||||
part="textarea"
|
||||
ref={el => (this.textarea = el)}
|
||||
id={this.textareaId}
|
||||
id={this.inputId}
|
||||
class="textarea__control"
|
||||
name={this.name}
|
||||
placeholder={this.placeholder}
|
||||
@@ -308,21 +304,7 @@ export class Textarea {
|
||||
onBlur={this.handleBlur}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
part="help-text"
|
||||
id={this.helpTextId}
|
||||
class={{
|
||||
'help-text': true,
|
||||
'help-text--small': this.size === 'small',
|
||||
'help-text--medium': this.size === 'medium',
|
||||
'help-text--large': this.size === 'large',
|
||||
'help-text--invalid': this.invalid
|
||||
}}
|
||||
>
|
||||
<slot name="help-text" />
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
54
src/functional-components/form-control/form-control.scss
Normal file
54
src/functional-components/form-control/form-control.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
.form-control {
|
||||
.form-control__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-control__help-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Label
|
||||
.form-control--has-label {
|
||||
.form-control__label {
|
||||
display: inline-block;
|
||||
color: var(--sl-input-label-color);
|
||||
margin-bottom: var(--sl-spacing-xxx-small);
|
||||
}
|
||||
|
||||
&.form-control--small .form-control__label {
|
||||
font-size: var(--sl-input-label-font-size-small);
|
||||
}
|
||||
|
||||
&.form-control--medium .form-control__label {
|
||||
font-size: var(--sl-input-label-font-size-medium);
|
||||
}
|
||||
|
||||
&.form-control--large .form-control_label {
|
||||
font-size: var(--sl-input-label-font-size-large);
|
||||
}
|
||||
}
|
||||
|
||||
// Help text
|
||||
.form-control--has-help-text {
|
||||
.form-control__help-text {
|
||||
display: block;
|
||||
color: var(--sl-input-help-text-color);
|
||||
|
||||
::slotted(*) {
|
||||
margin-top: var(--sl-spacing-xxx-small);
|
||||
}
|
||||
}
|
||||
|
||||
&.form-control--small .form-control__help-text {
|
||||
font-size: var(--sl-input-help-text-font-size-small);
|
||||
}
|
||||
|
||||
&.form-control--medium .form-control__help-text {
|
||||
font-size: var(--sl-input-help-text-font-size-medium);
|
||||
}
|
||||
|
||||
&.form-control--large .form-control__help-text {
|
||||
font-size: var(--sl-input-help-text-font-size-large);
|
||||
}
|
||||
}
|
||||
73
src/functional-components/form-control/form-control.tsx
Normal file
73
src/functional-components/form-control/form-control.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { h } from '@stencil/core';
|
||||
|
||||
export interface FormControlProps {
|
||||
/** The input id, used to map the input to the label */
|
||||
inputId: string;
|
||||
|
||||
/** The size of the form control */
|
||||
size: 'small' | 'medium' | 'large';
|
||||
|
||||
/** The label id, used to map the label to the input */
|
||||
labelId?: string;
|
||||
|
||||
/** The label text (if the label slot isn't used) */
|
||||
label?: string;
|
||||
|
||||
/** Whether or not a label slot has been provided. */
|
||||
hasLabelSlot?: boolean;
|
||||
|
||||
/** The help text id, used to map the input to the help text */
|
||||
helpTextId?: string;
|
||||
|
||||
/** The help text (if the help-text slot isn't used) */
|
||||
helpText?: string;
|
||||
|
||||
/** Whether or not a help text slot has been provided. */
|
||||
hasHelpTextSlot?: boolean;
|
||||
|
||||
/** A function that gets called when the label is clicked. */
|
||||
onLabelClick?: (event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
const FormControl = (props: FormControlProps, children) => {
|
||||
const hasLabel = props.label ? true : props.hasLabelSlot;
|
||||
const hasHelpText = props.helpText ? true : props.hasHelpTextSlot;
|
||||
|
||||
return (
|
||||
<div
|
||||
part="form-control"
|
||||
class={{
|
||||
'form-control': true,
|
||||
'form-control--small': props.size === 'small',
|
||||
'form-control--medium': props.size === 'medium',
|
||||
'form-control--large': props.size === 'large',
|
||||
'form-control--has-label': hasLabel,
|
||||
'form-control--has-help-text': hasHelpText
|
||||
}}
|
||||
>
|
||||
<label
|
||||
part="label"
|
||||
id={props.labelId}
|
||||
class="form-control__label"
|
||||
htmlFor={props.inputId}
|
||||
aria-hidden={hasLabel ? 'false' : 'true'}
|
||||
onClick={props.onLabelClick}
|
||||
>
|
||||
<slot name="label">{props.label}</slot>
|
||||
</label>
|
||||
|
||||
<div class="form-control__input">{children}</div>
|
||||
|
||||
<div
|
||||
part="help-text"
|
||||
id={props.helpTextId}
|
||||
class="form-control__help-text"
|
||||
aria-hidden={hasHelpText ? 'false' : 'true'}
|
||||
>
|
||||
<slot name="help-text">{props.helpText}</slot>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormControl;
|
||||
@@ -1,23 +0,0 @@
|
||||
.help-text {
|
||||
color: var(--sl-input-help-text-color);
|
||||
|
||||
&.help-text--small {
|
||||
font-size: var(--sl-input-help-text-font-size-small);
|
||||
}
|
||||
|
||||
&.help-text--medium {
|
||||
font-size: var(--sl-input-help-text-font-size-medium);
|
||||
}
|
||||
|
||||
&.help-text--large {
|
||||
font-size: var(--sl-input-help-text-font-size-large);
|
||||
}
|
||||
|
||||
&.help-text--invalid {
|
||||
color: var(--sl-input-help-text-color-invalid);
|
||||
}
|
||||
|
||||
::slotted(*) {
|
||||
margin-top: var(--sl-spacing-xxx-small);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
.form-control {
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control--has-label {
|
||||
.label {
|
||||
display: inline-block;
|
||||
color: var(--sl-input-label-color);
|
||||
margin-bottom: var(--sl-spacing-xxx-small);
|
||||
|
||||
&.label--small {
|
||||
font-size: var(--sl-input-label-font-size-small);
|
||||
}
|
||||
|
||||
&.label--medium {
|
||||
font-size: var(--sl-input-label-font-size-medium);
|
||||
}
|
||||
|
||||
&.label--large {
|
||||
font-size: var(--sl-input-label-font-size-large);
|
||||
}
|
||||
|
||||
&.label--invalid {
|
||||
color: var(--sl-input-label-color-invalid);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user