kopia lustrzana https://github.com/shoelace-style/shoelace
use reactive controller for slot detection
rodzic
1e3bac6031
commit
46f05224ab
|
@ -19,6 +19,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
|||
- Improved `<sl-spinner>` track color when used on various backgrounds
|
||||
- Improved a11y in `<sl-radio>` so VoiceOver announces radios properly in a radio group
|
||||
- Improved the API for the experimental `<sl-split-panel>` component by making `position` accept a percentage and adding the `position-in-pixels` attribute
|
||||
- Refactored `<sl-breadcrumb-item>`, `<sl-button>`, `<sl-card>`, `<sl-dialog>`, `<sl-drawer>`, `<sl-input>`, `<sl-range>`, `<sl-select>`, and `<sl-textarea>` to use a Reactive Controller for slot detection
|
||||
- Refactored internal id usage in `<sl-details>`, `<sl-dialog>`, `<sl-drawer>`, and `<sl-dropdown>`
|
||||
- Removed `position: relative` from the common component stylesheet
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './breadcrumb-item.styles';
|
||||
|
||||
/**
|
||||
|
@ -25,8 +25,7 @@ import styles from './breadcrumb-item.styles';
|
|||
export default class SlBreadcrumbItem extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
@state() hasPrefix = false;
|
||||
@state() hasSuffix = false;
|
||||
private hasSlotController = new HasSlotController(this, ['prefix', 'suffix']);
|
||||
|
||||
/**
|
||||
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered
|
||||
|
@ -40,11 +39,6 @@ export default class SlBreadcrumbItem extends LitElement {
|
|||
/** The `rel` attribute to use on the link. Only used when `href` is set. */
|
||||
@property() rel: string = 'noreferrer noopener';
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasPrefix = hasSlot(this, 'prefix');
|
||||
this.hasSuffix = hasSlot(this, 'suffix');
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLink = this.href ? true : false;
|
||||
|
||||
|
@ -53,12 +47,12 @@ export default class SlBreadcrumbItem extends LitElement {
|
|||
part="base"
|
||||
class=${classMap({
|
||||
'breadcrumb-item': true,
|
||||
'breadcrumb-item--has-prefix': this.hasPrefix,
|
||||
'breadcrumb-item--has-suffix': this.hasSuffix
|
||||
'breadcrumb-item--has-prefix': this.hasSlotController.test('prefix'),
|
||||
'breadcrumb-item--has-suffix': this.hasSlotController.test('suffix')
|
||||
})}
|
||||
>
|
||||
<span part="prefix" class="breadcrumb-item__prefix">
|
||||
<slot name="prefix" @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="prefix"></slot>
|
||||
</span>
|
||||
|
||||
${isLink
|
||||
|
@ -80,7 +74,7 @@ export default class SlBreadcrumbItem extends LitElement {
|
|||
`}
|
||||
|
||||
<span part="suffix" class="breadcrumb-item__suffix">
|
||||
<slot name="suffix" @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="suffix"></slot>
|
||||
</span>
|
||||
|
||||
<span part="separator" class="breadcrumb-item__separator" aria-hidden="true">
|
||||
|
|
|
@ -4,7 +4,7 @@ import { html, literal } from 'lit/static-html.js';
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './button.styles';
|
||||
|
||||
import '../spinner/spinner';
|
||||
|
@ -34,10 +34,9 @@ export default class SlButton extends LitElement {
|
|||
|
||||
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, ['[default]', 'prefix', 'suffix']);
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() private hasLabel = false;
|
||||
@state() private hasPrefix = false;
|
||||
@state() private hasSuffix = false;
|
||||
|
||||
/** The button's variant. */
|
||||
@property({ reflect: true }) variant: 'default' | 'primary' | 'success' | 'neutral' | 'warning' | 'danger' | 'text' =
|
||||
|
@ -82,11 +81,6 @@ export default class SlButton extends LitElement {
|
|||
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
|
||||
@property() download: string;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
/** Simulates a click on the button. */
|
||||
click() {
|
||||
this.button.click();
|
||||
|
@ -102,12 +96,6 @@ export default class SlButton extends LitElement {
|
|||
this.button.blur();
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasLabel = hasSlot(this);
|
||||
this.hasPrefix = hasSlot(this, 'prefix');
|
||||
this.hasSuffix = hasSlot(this, 'suffix');
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.hasFocus = false;
|
||||
emit(this, 'sl-blur');
|
||||
|
@ -152,9 +140,9 @@ export default class SlButton extends LitElement {
|
|||
'button--standard': !this.outline,
|
||||
'button--outline': this.outline,
|
||||
'button--pill': this.pill,
|
||||
'button--has-label': this.hasLabel,
|
||||
'button--has-prefix': this.hasPrefix,
|
||||
'button--has-suffix': this.hasSuffix
|
||||
'button--has-label': this.hasSlotController.test('[default]'),
|
||||
'button--has-prefix': this.hasSlotController.test('prefix'),
|
||||
'button--has-suffix': this.hasSlotController.test('suffix')
|
||||
})}
|
||||
?disabled=${ifDefined(isLink ? undefined : this.disabled)}
|
||||
type=${ifDefined(isLink ? undefined : this.submit ? 'submit' : 'button')}
|
||||
|
@ -172,13 +160,13 @@ export default class SlButton extends LitElement {
|
|||
@click=${this.handleClick}
|
||||
>
|
||||
<span part="prefix" class="button__prefix">
|
||||
<slot @slotchange=${this.handleSlotChange} name="prefix"></slot>
|
||||
<slot name="prefix"></slot>
|
||||
</span>
|
||||
<span part="label" class="button__label">
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot></slot>
|
||||
</span>
|
||||
<span part="suffix" class="button__suffix">
|
||||
<slot @slotchange=${this.handleSlotChange} name="suffix"></slot>
|
||||
<slot name="suffix"></slot>
|
||||
</span>
|
||||
${
|
||||
this.caret
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './card.styles';
|
||||
|
||||
/**
|
||||
|
@ -28,15 +28,7 @@ import styles from './card.styles';
|
|||
export default class SlCard extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
@state() private hasFooter = false;
|
||||
@state() private hasImage = false;
|
||||
@state() private hasHeader = false;
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasFooter = hasSlot(this, 'footer');
|
||||
this.hasImage = hasSlot(this, 'image');
|
||||
this.hasHeader = hasSlot(this, 'header');
|
||||
}
|
||||
private hasSlotController = new HasSlotController(this, ['footer', 'header', 'image']);
|
||||
|
||||
render() {
|
||||
return html`
|
||||
|
@ -44,17 +36,17 @@ export default class SlCard extends LitElement {
|
|||
part="base"
|
||||
class=${classMap({
|
||||
card: true,
|
||||
'card--has-footer': this.hasFooter,
|
||||
'card--has-image': this.hasImage,
|
||||
'card--has-header': this.hasHeader
|
||||
'card--has-footer': this.hasSlotController.test('footer'),
|
||||
'card--has-image': this.hasSlotController.test('image'),
|
||||
'card--has-header': this.hasSlotController.test('header')
|
||||
})}
|
||||
>
|
||||
<div part="image" class="card__image">
|
||||
<slot name="image" @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="image"></slot>
|
||||
</div>
|
||||
|
||||
<div part="header" class="card__header">
|
||||
<slot name="header" @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
|
||||
<div part="body" class="card__body">
|
||||
|
@ -62,7 +54,7 @@ export default class SlCard extends LitElement {
|
|||
</div>
|
||||
|
||||
<div part="footer" class="card__footer">
|
||||
<slot name="footer" @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
|
@ -7,7 +7,7 @@ import { emit } from '../../internal/event';
|
|||
import { watch } from '../../internal/watch';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { isPreventScrollSupported } from '../../internal/support';
|
||||
import Modal from '../../internal/modal';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
|
@ -65,11 +65,10 @@ export default class SlDialog extends LitElement {
|
|||
@query('.dialog__panel') panel: HTMLElement;
|
||||
@query('.dialog__overlay') overlay: HTMLElement;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, ['footer']);
|
||||
private modal: Modal;
|
||||
private originalTrigger: HTMLElement | null;
|
||||
|
||||
@state() private hasFooter = false;
|
||||
|
||||
/** Indicates whether or not the dialog is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
|
@ -87,9 +86,7 @@ export default class SlDialog extends LitElement {
|
|||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.modal = new Modal(this);
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
|
@ -208,10 +205,6 @@ export default class SlDialog extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasFooter = hasSlot(this, 'footer');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
|
@ -219,7 +212,7 @@ export default class SlDialog extends LitElement {
|
|||
class=${classMap({
|
||||
dialog: true,
|
||||
'dialog--open': this.open,
|
||||
'dialog--has-footer': this.hasFooter
|
||||
'dialog--has-footer': this.hasSlotController.test('footer')
|
||||
})}
|
||||
@keydown=${this.handleKeyDown}
|
||||
>
|
||||
|
@ -257,7 +250,7 @@ export default class SlDialog extends LitElement {
|
|||
</div>
|
||||
|
||||
<footer part="footer" class="dialog__footer">
|
||||
<slot name="footer" @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
|
@ -7,7 +7,7 @@ import { emit } from '../../internal/event';
|
|||
import { watch } from '../../internal/watch';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { uppercaseFirstLetter } from '../../internal/string';
|
||||
import { isPreventScrollSupported } from '../../internal/support';
|
||||
import Modal from '../../internal/modal';
|
||||
|
@ -73,11 +73,10 @@ export default class SlDrawer extends LitElement {
|
|||
@query('.drawer__panel') panel: HTMLElement;
|
||||
@query('.drawer__overlay') overlay: HTMLElement;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, ['footer']);
|
||||
private modal: Modal;
|
||||
private originalTrigger: HTMLElement | null;
|
||||
|
||||
@state() private hasFooter = false;
|
||||
|
||||
/** Indicates whether or not the drawer is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
|
@ -104,9 +103,7 @@ export default class SlDrawer extends LitElement {
|
|||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.modal = new Modal(this);
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
|
@ -228,10 +225,6 @@ export default class SlDrawer extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasFooter = hasSlot(this, 'footer');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
|
@ -245,7 +238,7 @@ export default class SlDrawer extends LitElement {
|
|||
'drawer--start': this.placement === 'start',
|
||||
'drawer--contained': this.contained,
|
||||
'drawer--fixed': !this.contained,
|
||||
'drawer--has-footer': this.hasFooter
|
||||
'drawer--has-footer': this.hasSlotController.test('footer')
|
||||
})}
|
||||
@keydown=${this.handleKeyDown}
|
||||
>
|
||||
|
@ -284,7 +277,7 @@ export default class SlDrawer extends LitElement {
|
|||
</div>
|
||||
|
||||
<footer part="footer" class="drawer__footer">
|
||||
<slot name="footer" @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { live } from 'lit/directives/live.js';
|
|||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './input.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
|
@ -49,13 +49,12 @@ export default class SlInput extends LitElement {
|
|||
|
||||
@query('.input__control') input: HTMLInputElement;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, ['help-text', 'label']);
|
||||
private inputId = `input-${++id}`;
|
||||
private helpTextId = `input-help-text-${id}`;
|
||||
private labelId = `input-label-${id}`;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() private hasHelpTextSlot = false;
|
||||
@state() private hasLabelSlot = false;
|
||||
@state() private isPasswordVisible = false;
|
||||
|
||||
/** The input's type. */
|
||||
|
@ -163,21 +162,10 @@ export default class SlInput extends LitElement {
|
|||
this.value = this.input.value;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
/** Sets focus on the input. */
|
||||
focus(options?: FocusOptions) {
|
||||
this.input.focus(options);
|
||||
|
@ -276,13 +264,6 @@ export default class SlInput extends LitElement {
|
|||
this.isPasswordVisible = !this.isPasswordVisible;
|
||||
}
|
||||
|
||||
@watch('helpText')
|
||||
@watch('label')
|
||||
handleSlotChange() {
|
||||
this.hasHelpTextSlot = hasSlot(this, 'help-text');
|
||||
this.hasLabelSlot = hasSlot(this, 'label');
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
handleValueChange() {
|
||||
if (this.input) {
|
||||
|
@ -291,16 +272,19 @@ export default class SlInput extends LitElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
const hasLabelSlot = this.hasSlotController.test('label');
|
||||
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||
|
||||
// NOTE - always bind value after min/max, otherwise it will be clamped
|
||||
return renderFormControl(
|
||||
{
|
||||
inputId: this.inputId,
|
||||
label: this.label,
|
||||
labelId: this.labelId,
|
||||
hasLabelSlot: this.hasLabelSlot,
|
||||
hasLabelSlot,
|
||||
helpTextId: this.helpTextId,
|
||||
helpText: this.helpText,
|
||||
hasHelpTextSlot: this.hasHelpTextSlot,
|
||||
hasHelpTextSlot,
|
||||
size: this.size
|
||||
},
|
||||
html`
|
||||
|
@ -355,10 +339,10 @@ export default class SlInput extends LitElement {
|
|||
getLabelledBy({
|
||||
label: this.label,
|
||||
labelId: this.labelId,
|
||||
hasLabelSlot: this.hasLabelSlot,
|
||||
hasLabelSlot,
|
||||
helpText: this.helpText,
|
||||
helpTextId: this.helpTextId,
|
||||
hasHelpTextSlot: this.hasHelpTextSlot
|
||||
hasHelpTextSlot
|
||||
})
|
||||
)}
|
||||
aria-invalid=${this.invalid ? 'true' : 'false'}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { emit } from '../../internal/event';
|
|||
import { live } from 'lit/directives/live.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './range.styles';
|
||||
|
||||
let id = 0;
|
||||
|
@ -39,14 +39,13 @@ export default class SlRange extends LitElement {
|
|||
@query('.range__control') input: HTMLInputElement;
|
||||
@query('.range__tooltip') output: HTMLOutputElement;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, ['help-text', 'label']);
|
||||
private inputId = `input-${++id}`;
|
||||
private helpTextId = `input-help-text-${id}`;
|
||||
private labelId = `input-label-${id}`;
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() private hasHelpTextSlot = false;
|
||||
@state() private hasLabelSlot = false;
|
||||
@state() private hasTooltip = false;
|
||||
|
||||
/** The input's name attribute. */
|
||||
|
@ -88,14 +87,11 @@ export default class SlRange extends LitElement {
|
|||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.resizeObserver = new ResizeObserver(() => this.syncRange());
|
||||
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
|
||||
if (this.value === undefined || this.value === null) this.value = this.min;
|
||||
if (this.value < this.min) this.value = this.min;
|
||||
if (this.value > this.max) this.value = this.max;
|
||||
|
||||
this.handleSlotChange();
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
this.syncRange();
|
||||
this.resizeObserver.observe(this.input);
|
||||
|
@ -105,7 +101,6 @@ export default class SlRange extends LitElement {
|
|||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.resizeObserver.unobserve(this.input);
|
||||
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
/** Sets focus on the input. */
|
||||
|
@ -163,13 +158,6 @@ export default class SlRange extends LitElement {
|
|||
emit(this, 'sl-focus');
|
||||
}
|
||||
|
||||
@watch('label')
|
||||
@watch('helpText')
|
||||
handleSlotChange() {
|
||||
this.hasHelpTextSlot = hasSlot(this, 'help-text');
|
||||
this.hasLabelSlot = hasSlot(this, 'label');
|
||||
}
|
||||
|
||||
handleThumbDragStart() {
|
||||
this.hasTooltip = true;
|
||||
}
|
||||
|
@ -207,16 +195,19 @@ export default class SlRange extends LitElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
const hasLabelSlot = this.hasSlotController.test('label');
|
||||
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||
|
||||
// NOTE - always bind value after min/max, otherwise it will be clamped
|
||||
return renderFormControl(
|
||||
{
|
||||
inputId: this.inputId,
|
||||
label: this.label,
|
||||
labelId: this.labelId,
|
||||
hasLabelSlot: this.hasLabelSlot,
|
||||
hasLabelSlot,
|
||||
helpTextId: this.helpTextId,
|
||||
helpText: this.helpText,
|
||||
hasHelpTextSlot: this.hasHelpTextSlot,
|
||||
hasHelpTextSlot,
|
||||
size: 'medium'
|
||||
},
|
||||
html`
|
||||
|
@ -249,10 +240,10 @@ export default class SlRange extends LitElement {
|
|||
getLabelledBy({
|
||||
label: this.label,
|
||||
labelId: this.labelId,
|
||||
hasLabelSlot: this.hasLabelSlot,
|
||||
hasLabelSlot,
|
||||
helpText: this.helpText,
|
||||
helpTextId: this.helpTextId,
|
||||
hasHelpTextSlot: this.hasHelpTextSlot
|
||||
hasHelpTextSlot
|
||||
})
|
||||
)}
|
||||
@input=${this.handleInput}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { emit } from '../../internal/event';
|
|||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlIconButton from '../icon-button/icon-button';
|
||||
import type SlMenu from '../menu/menu';
|
||||
|
@ -65,14 +65,13 @@ export default class SlSelect extends LitElement {
|
|||
@query('.select__hidden-select') input: HTMLInputElement;
|
||||
@query('.select__menu') menu: SlMenu;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, ['help-text', 'label']);
|
||||
private inputId = `select-${++id}`;
|
||||
private helpTextId = `select-help-text-${id}`;
|
||||
private labelId = `select-label-${id}`;
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() private hasHelpTextSlot = false;
|
||||
@state() private hasLabelSlot = false;
|
||||
@state() private isOpen = false;
|
||||
@state() private displayLabel = '';
|
||||
@state() private displayTags: TemplateResult[] = [];
|
||||
|
@ -130,12 +129,11 @@ export default class SlSelect extends LitElement {
|
|||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
this.handleMenuSlotChange = this.handleMenuSlotChange.bind(this);
|
||||
this.resizeObserver = new ResizeObserver(() => this.resizeMenu());
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
this.resizeObserver.observe(this);
|
||||
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
this.syncItemsFromValue();
|
||||
});
|
||||
}
|
||||
|
@ -147,7 +145,6 @@ export default class SlSelect extends LitElement {
|
|||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.resizeObserver.unobserve(this);
|
||||
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
|
@ -318,12 +315,7 @@ export default class SlSelect extends LitElement {
|
|||
this.syncItemsFromValue();
|
||||
}
|
||||
|
||||
@watch('helpText')
|
||||
@watch('label')
|
||||
async handleSlotChange() {
|
||||
this.hasHelpTextSlot = hasSlot(this, 'help-text');
|
||||
this.hasLabelSlot = hasSlot(this, 'label');
|
||||
|
||||
async handleMenuSlotChange() {
|
||||
// Wait for items to render before gathering labels otherwise the slot won't exist
|
||||
const items = this.getItems();
|
||||
|
||||
|
@ -437,6 +429,8 @@ export default class SlSelect extends LitElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
const hasLabelSlot = this.hasSlotController.test('label');
|
||||
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||
const hasSelection = this.multiple ? this.value?.length > 0 : this.value !== '';
|
||||
|
||||
return renderFormControl(
|
||||
|
@ -444,10 +438,10 @@ export default class SlSelect extends LitElement {
|
|||
inputId: this.inputId,
|
||||
label: this.label,
|
||||
labelId: this.labelId,
|
||||
hasLabelSlot: this.hasLabelSlot,
|
||||
hasLabelSlot,
|
||||
helpTextId: this.helpTextId,
|
||||
helpText: this.helpText,
|
||||
hasHelpTextSlot: this.hasHelpTextSlot,
|
||||
hasHelpTextSlot,
|
||||
size: this.size,
|
||||
onLabelClick: () => this.handleLabelClick()
|
||||
},
|
||||
|
@ -489,10 +483,10 @@ export default class SlSelect extends LitElement {
|
|||
getLabelledBy({
|
||||
label: this.label,
|
||||
labelId: this.labelId,
|
||||
hasLabelSlot: this.hasLabelSlot,
|
||||
hasLabelSlot,
|
||||
helpText: this.helpText,
|
||||
helpTextId: this.helpTextId,
|
||||
hasHelpTextSlot: this.hasHelpTextSlot
|
||||
hasHelpTextSlot
|
||||
})
|
||||
)}
|
||||
aria-haspopup="true"
|
||||
|
@ -548,7 +542,7 @@ export default class SlSelect extends LitElement {
|
|||
</div>
|
||||
|
||||
<sl-menu part="menu" class="select__menu" @sl-select=${this.handleMenuSelect}>
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot @slotchange=${this.handleMenuSlotChange}></slot>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`
|
||||
|
|
|
@ -6,7 +6,7 @@ import { emit } from '../../internal/event';
|
|||
import { live } from 'lit/directives/live.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './textarea.styles';
|
||||
|
||||
let id = 0;
|
||||
|
@ -35,14 +35,13 @@ export default class SlTextarea extends LitElement {
|
|||
|
||||
@query('.textarea__control') input: HTMLTextAreaElement;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, ['help-text', 'label']);
|
||||
private inputId = `textarea-${++id}`;
|
||||
private helpTextId = `textarea-help-text-${id}`;
|
||||
private labelId = `textarea-label-${id}`;
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() private hasHelpTextSlot = false;
|
||||
@state() private hasLabelSlot = false;
|
||||
|
||||
/** The textarea's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
@ -115,10 +114,7 @@ export default class SlTextarea extends LitElement {
|
|||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight());
|
||||
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
this.handleSlotChange();
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
this.setTextareaHeight();
|
||||
|
@ -133,7 +129,6 @@ export default class SlTextarea extends LitElement {
|
|||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.resizeObserver.unobserve(this.input);
|
||||
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
/** Sets focus on the textarea. */
|
||||
|
@ -243,13 +238,6 @@ export default class SlTextarea extends LitElement {
|
|||
this.setTextareaHeight();
|
||||
}
|
||||
|
||||
@watch('helpText')
|
||||
@watch('label')
|
||||
handleSlotChange() {
|
||||
this.hasHelpTextSlot = hasSlot(this, 'help-text');
|
||||
this.hasLabelSlot = hasSlot(this, 'label');
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
handleValueChange() {
|
||||
if (this.input) {
|
||||
|
@ -269,15 +257,18 @@ export default class SlTextarea extends LitElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
const hasLabelSlot = this.hasSlotController.test('label');
|
||||
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||
|
||||
return renderFormControl(
|
||||
{
|
||||
inputId: this.inputId,
|
||||
label: this.label,
|
||||
labelId: this.labelId,
|
||||
hasLabelSlot: this.hasLabelSlot,
|
||||
hasLabelSlot,
|
||||
helpTextId: this.helpTextId,
|
||||
helpText: this.helpText,
|
||||
hasHelpTextSlot: this.hasHelpTextSlot,
|
||||
hasHelpTextSlot,
|
||||
size: this.size
|
||||
},
|
||||
html`
|
||||
|
@ -321,10 +312,10 @@ export default class SlTextarea extends LitElement {
|
|||
getLabelledBy({
|
||||
label: this.label,
|
||||
labelId: this.labelId,
|
||||
hasLabelSlot: this.hasLabelSlot,
|
||||
hasLabelSlot,
|
||||
helpText: this.helpText,
|
||||
helpTextId: this.helpTextId,
|
||||
hasHelpTextSlot: this.hasHelpTextSlot
|
||||
hasHelpTextSlot
|
||||
})
|
||||
)}
|
||||
@change=${this.handleChange}
|
||||
|
|
|
@ -1,3 +1,57 @@
|
|||
import { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||
|
||||
export class HasSlotController implements ReactiveController {
|
||||
host: ReactiveControllerHost & Element;
|
||||
slotNames: string[] = [];
|
||||
|
||||
constructor(host: ReactiveControllerHost & Element, slotNames: string[] = []) {
|
||||
(this.host = host).addController(this);
|
||||
this.slotNames = slotNames;
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
}
|
||||
|
||||
private hasDefaultSlot() {
|
||||
return [...this.host.childNodes].some(node => {
|
||||
if (node.nodeType === node.TEXT_NODE && node.textContent!.trim() !== '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.nodeType === node.ELEMENT_NODE) {
|
||||
const el = node as HTMLElement;
|
||||
if (!el.hasAttribute('slot')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private hasNamedSlot(name: string) {
|
||||
return this.host.querySelector(`:scope > [slot="${name}"]`) !== null;
|
||||
}
|
||||
|
||||
test(slotName: string) {
|
||||
return slotName === '[default]' ? this.hasDefaultSlot() : this.hasNamedSlot(slotName);
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.host.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
handleSlotChange(event: Event) {
|
||||
const slot = event.target as HTMLSlotElement;
|
||||
|
||||
if ((this.slotNames.includes('[default]') && !slot.name) || (slot.name && this.slotNames?.includes(slot.name))) {
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Given a slot, this function iterates over all of its assigned element and text nodes and returns the concatenated
|
||||
// HTML as a string. This is useful because we can't use slot.innerHTML as an alternative.
|
||||
|
@ -35,30 +89,3 @@ export function getTextContent(slot: HTMLSlotElement): string {
|
|||
|
||||
return text;
|
||||
}
|
||||
|
||||
//
|
||||
// Determines whether an element has a slot. If name is specified, the function will look for a corresponding named
|
||||
// slot, otherwise it will look for a "default" slot (e.g. a non-empty text node or an element with no slot attribute).
|
||||
//
|
||||
export function hasSlot(el: HTMLElement, name?: string) {
|
||||
// Look for a named slot
|
||||
if (name) {
|
||||
return el.querySelector(`:scope > [slot="${name}"]`) !== null;
|
||||
}
|
||||
|
||||
// Look for a default slot
|
||||
return [...el.childNodes].some(node => {
|
||||
if (node.nodeType === node.TEXT_NODE && node.textContent!.trim() !== '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.nodeType === node.ELEMENT_NODE) {
|
||||
const el = node as HTMLElement;
|
||||
if (!el.hasAttribute('slot')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue