use reactive controller for slot detection

pull/642/head
Cory LaViska 2022-01-05 18:31:41 -05:00
rodzic 1e3bac6031
commit 46f05224ab
11 zmienionych plików z 128 dodań i 180 usunięć

Wyświetl plik

@ -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 `<sl-spinner>` track color when used on various backgrounds
- Improved a11y in `<sl-radio>` so VoiceOver announces radios properly in a radio group - 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 - 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>` - Refactored internal id usage in `<sl-details>`, `<sl-dialog>`, `<sl-drawer>`, and `<sl-dropdown>`
- Removed `position: relative` from the common component stylesheet - Removed `position: relative` from the common component stylesheet

Wyświetl plik

@ -1,8 +1,8 @@
import { LitElement, html } from 'lit'; 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 { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.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'; import styles from './breadcrumb-item.styles';
/** /**
@ -25,8 +25,7 @@ import styles from './breadcrumb-item.styles';
export default class SlBreadcrumbItem extends LitElement { export default class SlBreadcrumbItem extends LitElement {
static styles = styles; static styles = styles;
@state() hasPrefix = false; private hasSlotController = new HasSlotController(this, ['prefix', 'suffix']);
@state() hasSuffix = false;
/** /**
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered * 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. */ /** The `rel` attribute to use on the link. Only used when `href` is set. */
@property() rel: string = 'noreferrer noopener'; @property() rel: string = 'noreferrer noopener';
handleSlotChange() {
this.hasPrefix = hasSlot(this, 'prefix');
this.hasSuffix = hasSlot(this, 'suffix');
}
render() { render() {
const isLink = this.href ? true : false; const isLink = this.href ? true : false;
@ -53,12 +47,12 @@ export default class SlBreadcrumbItem extends LitElement {
part="base" part="base"
class=${classMap({ class=${classMap({
'breadcrumb-item': true, 'breadcrumb-item': true,
'breadcrumb-item--has-prefix': this.hasPrefix, 'breadcrumb-item--has-prefix': this.hasSlotController.test('prefix'),
'breadcrumb-item--has-suffix': this.hasSuffix 'breadcrumb-item--has-suffix': this.hasSlotController.test('suffix')
})} })}
> >
<span part="prefix" class="breadcrumb-item__prefix"> <span part="prefix" class="breadcrumb-item__prefix">
<slot name="prefix" @slotchange=${this.handleSlotChange}></slot> <slot name="prefix"></slot>
</span> </span>
${isLink ${isLink
@ -80,7 +74,7 @@ export default class SlBreadcrumbItem extends LitElement {
`} `}
<span part="suffix" class="breadcrumb-item__suffix"> <span part="suffix" class="breadcrumb-item__suffix">
<slot name="suffix" @slotchange=${this.handleSlotChange}></slot> <slot name="suffix"></slot>
</span> </span>
<span part="separator" class="breadcrumb-item__separator" aria-hidden="true"> <span part="separator" class="breadcrumb-item__separator" aria-hidden="true">

Wyświetl plik

@ -4,7 +4,7 @@ import { html, literal } from 'lit/static-html.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { emit } from '../../internal/event'; import { emit } from '../../internal/event';
import { hasSlot } from '../../internal/slot'; import { HasSlotController } from '../../internal/slot';
import styles from './button.styles'; import styles from './button.styles';
import '../spinner/spinner'; import '../spinner/spinner';
@ -34,10 +34,9 @@ export default class SlButton extends LitElement {
@query('.button') button: HTMLButtonElement | HTMLLinkElement; @query('.button') button: HTMLButtonElement | HTMLLinkElement;
private hasSlotController = new HasSlotController(this, ['[default]', 'prefix', 'suffix']);
@state() private hasFocus = false; @state() private hasFocus = false;
@state() private hasLabel = false;
@state() private hasPrefix = false;
@state() private hasSuffix = false;
/** The button's variant. */ /** The button's variant. */
@property({ reflect: true }) variant: 'default' | 'primary' | 'success' | 'neutral' | 'warning' | 'danger' | 'text' = @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. */ /** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
@property() download: string; @property() download: string;
connectedCallback() {
super.connectedCallback();
this.handleSlotChange();
}
/** Simulates a click on the button. */ /** Simulates a click on the button. */
click() { click() {
this.button.click(); this.button.click();
@ -102,12 +96,6 @@ export default class SlButton extends LitElement {
this.button.blur(); this.button.blur();
} }
handleSlotChange() {
this.hasLabel = hasSlot(this);
this.hasPrefix = hasSlot(this, 'prefix');
this.hasSuffix = hasSlot(this, 'suffix');
}
handleBlur() { handleBlur() {
this.hasFocus = false; this.hasFocus = false;
emit(this, 'sl-blur'); emit(this, 'sl-blur');
@ -152,9 +140,9 @@ export default class SlButton extends LitElement {
'button--standard': !this.outline, 'button--standard': !this.outline,
'button--outline': this.outline, 'button--outline': this.outline,
'button--pill': this.pill, 'button--pill': this.pill,
'button--has-label': this.hasLabel, 'button--has-label': this.hasSlotController.test('[default]'),
'button--has-prefix': this.hasPrefix, 'button--has-prefix': this.hasSlotController.test('prefix'),
'button--has-suffix': this.hasSuffix 'button--has-suffix': this.hasSlotController.test('suffix')
})} })}
?disabled=${ifDefined(isLink ? undefined : this.disabled)} ?disabled=${ifDefined(isLink ? undefined : this.disabled)}
type=${ifDefined(isLink ? undefined : this.submit ? 'submit' : 'button')} type=${ifDefined(isLink ? undefined : this.submit ? 'submit' : 'button')}
@ -172,13 +160,13 @@ export default class SlButton extends LitElement {
@click=${this.handleClick} @click=${this.handleClick}
> >
<span part="prefix" class="button__prefix"> <span part="prefix" class="button__prefix">
<slot @slotchange=${this.handleSlotChange} name="prefix"></slot> <slot name="prefix"></slot>
</span> </span>
<span part="label" class="button__label"> <span part="label" class="button__label">
<slot @slotchange=${this.handleSlotChange}></slot> <slot></slot>
</span> </span>
<span part="suffix" class="button__suffix"> <span part="suffix" class="button__suffix">
<slot @slotchange=${this.handleSlotChange} name="suffix"></slot> <slot name="suffix"></slot>
</span> </span>
${ ${
this.caret this.caret

Wyświetl plik

@ -1,7 +1,7 @@
import { LitElement, html } from 'lit'; 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 { classMap } from 'lit/directives/class-map.js';
import { hasSlot } from '../../internal/slot'; import { HasSlotController } from '../../internal/slot';
import styles from './card.styles'; import styles from './card.styles';
/** /**
@ -28,15 +28,7 @@ import styles from './card.styles';
export default class SlCard extends LitElement { export default class SlCard extends LitElement {
static styles = styles; static styles = styles;
@state() private hasFooter = false; private hasSlotController = new HasSlotController(this, ['footer', 'header', 'image']);
@state() private hasImage = false;
@state() private hasHeader = false;
handleSlotChange() {
this.hasFooter = hasSlot(this, 'footer');
this.hasImage = hasSlot(this, 'image');
this.hasHeader = hasSlot(this, 'header');
}
render() { render() {
return html` return html`
@ -44,17 +36,17 @@ export default class SlCard extends LitElement {
part="base" part="base"
class=${classMap({ class=${classMap({
card: true, card: true,
'card--has-footer': this.hasFooter, 'card--has-footer': this.hasSlotController.test('footer'),
'card--has-image': this.hasImage, 'card--has-image': this.hasSlotController.test('image'),
'card--has-header': this.hasHeader 'card--has-header': this.hasSlotController.test('header')
})} })}
> >
<div part="image" class="card__image"> <div part="image" class="card__image">
<slot name="image" @slotchange=${this.handleSlotChange}></slot> <slot name="image"></slot>
</div> </div>
<div part="header" class="card__header"> <div part="header" class="card__header">
<slot name="header" @slotchange=${this.handleSlotChange}></slot> <slot name="header"></slot>
</div> </div>
<div part="body" class="card__body"> <div part="body" class="card__body">
@ -62,7 +54,7 @@ export default class SlCard extends LitElement {
</div> </div>
<div part="footer" class="card__footer"> <div part="footer" class="card__footer">
<slot name="footer" @slotchange=${this.handleSlotChange}></slot> <slot name="footer"></slot>
</div> </div>
</div> </div>
`; `;

Wyświetl plik

@ -1,5 +1,5 @@
import { LitElement, html } from 'lit'; 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 { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { animateTo, stopAnimations } from '../../internal/animate'; import { animateTo, stopAnimations } from '../../internal/animate';
@ -7,7 +7,7 @@ import { emit } from '../../internal/event';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { waitForEvent } from '../../internal/event'; import { waitForEvent } from '../../internal/event';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll'; import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { hasSlot } from '../../internal/slot'; import { HasSlotController } from '../../internal/slot';
import { isPreventScrollSupported } from '../../internal/support'; import { isPreventScrollSupported } from '../../internal/support';
import Modal from '../../internal/modal'; import Modal from '../../internal/modal';
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry'; import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
@ -65,11 +65,10 @@ export default class SlDialog extends LitElement {
@query('.dialog__panel') panel: HTMLElement; @query('.dialog__panel') panel: HTMLElement;
@query('.dialog__overlay') overlay: HTMLElement; @query('.dialog__overlay') overlay: HTMLElement;
private hasSlotController = new HasSlotController(this, ['footer']);
private modal: Modal; private modal: Modal;
private originalTrigger: HTMLElement | null; 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. */ /** 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; @property({ type: Boolean, reflect: true }) open = false;
@ -87,9 +86,7 @@ export default class SlDialog extends LitElement {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.modal = new Modal(this); this.modal = new Modal(this);
this.handleSlotChange();
} }
firstUpdated() { firstUpdated() {
@ -208,10 +205,6 @@ export default class SlDialog extends LitElement {
} }
} }
handleSlotChange() {
this.hasFooter = hasSlot(this, 'footer');
}
render() { render() {
return html` return html`
<div <div
@ -219,7 +212,7 @@ export default class SlDialog extends LitElement {
class=${classMap({ class=${classMap({
dialog: true, dialog: true,
'dialog--open': this.open, 'dialog--open': this.open,
'dialog--has-footer': this.hasFooter 'dialog--has-footer': this.hasSlotController.test('footer')
})} })}
@keydown=${this.handleKeyDown} @keydown=${this.handleKeyDown}
> >
@ -257,7 +250,7 @@ export default class SlDialog extends LitElement {
</div> </div>
<footer part="footer" class="dialog__footer"> <footer part="footer" class="dialog__footer">
<slot name="footer" @slotchange=${this.handleSlotChange}></slot> <slot name="footer"></slot>
</footer> </footer>
</div> </div>
</div> </div>

Wyświetl plik

@ -1,5 +1,5 @@
import { LitElement, html } from 'lit'; 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 { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { animateTo, stopAnimations } from '../../internal/animate'; import { animateTo, stopAnimations } from '../../internal/animate';
@ -7,7 +7,7 @@ import { emit } from '../../internal/event';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { waitForEvent } from '../../internal/event'; import { waitForEvent } from '../../internal/event';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll'; import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { hasSlot } from '../../internal/slot'; import { HasSlotController } from '../../internal/slot';
import { uppercaseFirstLetter } from '../../internal/string'; import { uppercaseFirstLetter } from '../../internal/string';
import { isPreventScrollSupported } from '../../internal/support'; import { isPreventScrollSupported } from '../../internal/support';
import Modal from '../../internal/modal'; import Modal from '../../internal/modal';
@ -73,11 +73,10 @@ export default class SlDrawer extends LitElement {
@query('.drawer__panel') panel: HTMLElement; @query('.drawer__panel') panel: HTMLElement;
@query('.drawer__overlay') overlay: HTMLElement; @query('.drawer__overlay') overlay: HTMLElement;
private hasSlotController = new HasSlotController(this, ['footer']);
private modal: Modal; private modal: Modal;
private originalTrigger: HTMLElement | null; 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. */ /** 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; @property({ type: Boolean, reflect: true }) open = false;
@ -104,9 +103,7 @@ export default class SlDrawer extends LitElement {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.modal = new Modal(this); this.modal = new Modal(this);
this.handleSlotChange();
} }
firstUpdated() { firstUpdated() {
@ -228,10 +225,6 @@ export default class SlDrawer extends LitElement {
} }
} }
handleSlotChange() {
this.hasFooter = hasSlot(this, 'footer');
}
render() { render() {
return html` return html`
<div <div
@ -245,7 +238,7 @@ export default class SlDrawer extends LitElement {
'drawer--start': this.placement === 'start', 'drawer--start': this.placement === 'start',
'drawer--contained': this.contained, 'drawer--contained': this.contained,
'drawer--fixed': !this.contained, 'drawer--fixed': !this.contained,
'drawer--has-footer': this.hasFooter 'drawer--has-footer': this.hasSlotController.test('footer')
})} })}
@keydown=${this.handleKeyDown} @keydown=${this.handleKeyDown}
> >
@ -284,7 +277,7 @@ export default class SlDrawer extends LitElement {
</div> </div>
<footer part="footer" class="drawer__footer"> <footer part="footer" class="drawer__footer">
<slot name="footer" @slotchange=${this.handleSlotChange}></slot> <slot name="footer"></slot>
</footer> </footer>
</div> </div>
</div> </div>

Wyświetl plik

@ -6,7 +6,7 @@ import { live } from 'lit/directives/live.js';
import { emit } from '../../internal/event'; import { emit } from '../../internal/event';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { getLabelledBy, renderFormControl } from '../../internal/form-control'; import { getLabelledBy, renderFormControl } from '../../internal/form-control';
import { hasSlot } from '../../internal/slot'; import { HasSlotController } from '../../internal/slot';
import styles from './input.styles'; import styles from './input.styles';
import '../icon/icon'; import '../icon/icon';
@ -49,13 +49,12 @@ export default class SlInput extends LitElement {
@query('.input__control') input: HTMLInputElement; @query('.input__control') input: HTMLInputElement;
private hasSlotController = new HasSlotController(this, ['help-text', 'label']);
private inputId = `input-${++id}`; private inputId = `input-${++id}`;
private helpTextId = `input-help-text-${id}`; private helpTextId = `input-help-text-${id}`;
private labelId = `input-label-${id}`; private labelId = `input-label-${id}`;
@state() private hasFocus = false; @state() private hasFocus = false;
@state() private hasHelpTextSlot = false;
@state() private hasLabelSlot = false;
@state() private isPasswordVisible = false; @state() private isPasswordVisible = false;
/** The input's type. */ /** The input's type. */
@ -163,21 +162,10 @@ export default class SlInput extends LitElement {
this.value = this.input.value; this.value = this.input.value;
} }
connectedCallback() {
super.connectedCallback();
this.handleSlotChange = this.handleSlotChange.bind(this);
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
}
firstUpdated() { firstUpdated() {
this.invalid = !this.input.checkValidity(); this.invalid = !this.input.checkValidity();
} }
disconnectedCallback() {
super.disconnectedCallback();
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
}
/** Sets focus on the input. */ /** Sets focus on the input. */
focus(options?: FocusOptions) { focus(options?: FocusOptions) {
this.input.focus(options); this.input.focus(options);
@ -276,13 +264,6 @@ export default class SlInput extends LitElement {
this.isPasswordVisible = !this.isPasswordVisible; this.isPasswordVisible = !this.isPasswordVisible;
} }
@watch('helpText')
@watch('label')
handleSlotChange() {
this.hasHelpTextSlot = hasSlot(this, 'help-text');
this.hasLabelSlot = hasSlot(this, 'label');
}
@watch('value') @watch('value')
handleValueChange() { handleValueChange() {
if (this.input) { if (this.input) {
@ -291,16 +272,19 @@ export default class SlInput extends LitElement {
} }
render() { 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 // NOTE - always bind value after min/max, otherwise it will be clamped
return renderFormControl( return renderFormControl(
{ {
inputId: this.inputId, inputId: this.inputId,
label: this.label, label: this.label,
labelId: this.labelId, labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot, hasLabelSlot,
helpTextId: this.helpTextId, helpTextId: this.helpTextId,
helpText: this.helpText, helpText: this.helpText,
hasHelpTextSlot: this.hasHelpTextSlot, hasHelpTextSlot,
size: this.size size: this.size
}, },
html` html`
@ -355,10 +339,10 @@ export default class SlInput extends LitElement {
getLabelledBy({ getLabelledBy({
label: this.label, label: this.label,
labelId: this.labelId, labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot, hasLabelSlot,
helpText: this.helpText, helpText: this.helpText,
helpTextId: this.helpTextId, helpTextId: this.helpTextId,
hasHelpTextSlot: this.hasHelpTextSlot hasHelpTextSlot
}) })
)} )}
aria-invalid=${this.invalid ? 'true' : 'false'} aria-invalid=${this.invalid ? 'true' : 'false'}

Wyświetl plik

@ -6,7 +6,7 @@ import { emit } from '../../internal/event';
import { live } from 'lit/directives/live.js'; import { live } from 'lit/directives/live.js';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { getLabelledBy, renderFormControl } from '../../internal/form-control'; import { getLabelledBy, renderFormControl } from '../../internal/form-control';
import { hasSlot } from '../../internal/slot'; import { HasSlotController } from '../../internal/slot';
import styles from './range.styles'; import styles from './range.styles';
let id = 0; let id = 0;
@ -39,14 +39,13 @@ export default class SlRange extends LitElement {
@query('.range__control') input: HTMLInputElement; @query('.range__control') input: HTMLInputElement;
@query('.range__tooltip') output: HTMLOutputElement; @query('.range__tooltip') output: HTMLOutputElement;
private hasSlotController = new HasSlotController(this, ['help-text', 'label']);
private inputId = `input-${++id}`; private inputId = `input-${++id}`;
private helpTextId = `input-help-text-${id}`; private helpTextId = `input-help-text-${id}`;
private labelId = `input-label-${id}`; private labelId = `input-label-${id}`;
private resizeObserver: ResizeObserver; private resizeObserver: ResizeObserver;
@state() private hasFocus = false; @state() private hasFocus = false;
@state() private hasHelpTextSlot = false;
@state() private hasLabelSlot = false;
@state() private hasTooltip = false; @state() private hasTooltip = false;
/** The input's name attribute. */ /** The input's name attribute. */
@ -88,14 +87,11 @@ export default class SlRange extends LitElement {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.resizeObserver = new ResizeObserver(() => this.syncRange()); 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 === undefined || this.value === null) this.value = this.min;
if (this.value < this.min) this.value = this.min; if (this.value < this.min) this.value = this.min;
if (this.value > this.max) this.value = this.max; if (this.value > this.max) this.value = this.max;
this.handleSlotChange();
this.updateComplete.then(() => { this.updateComplete.then(() => {
this.syncRange(); this.syncRange();
this.resizeObserver.observe(this.input); this.resizeObserver.observe(this.input);
@ -105,7 +101,6 @@ export default class SlRange extends LitElement {
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this.resizeObserver.unobserve(this.input); this.resizeObserver.unobserve(this.input);
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
} }
/** Sets focus on the input. */ /** Sets focus on the input. */
@ -163,13 +158,6 @@ export default class SlRange extends LitElement {
emit(this, 'sl-focus'); emit(this, 'sl-focus');
} }
@watch('label')
@watch('helpText')
handleSlotChange() {
this.hasHelpTextSlot = hasSlot(this, 'help-text');
this.hasLabelSlot = hasSlot(this, 'label');
}
handleThumbDragStart() { handleThumbDragStart() {
this.hasTooltip = true; this.hasTooltip = true;
} }
@ -207,16 +195,19 @@ export default class SlRange extends LitElement {
} }
render() { 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 // NOTE - always bind value after min/max, otherwise it will be clamped
return renderFormControl( return renderFormControl(
{ {
inputId: this.inputId, inputId: this.inputId,
label: this.label, label: this.label,
labelId: this.labelId, labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot, hasLabelSlot,
helpTextId: this.helpTextId, helpTextId: this.helpTextId,
helpText: this.helpText, helpText: this.helpText,
hasHelpTextSlot: this.hasHelpTextSlot, hasHelpTextSlot,
size: 'medium' size: 'medium'
}, },
html` html`
@ -249,10 +240,10 @@ export default class SlRange extends LitElement {
getLabelledBy({ getLabelledBy({
label: this.label, label: this.label,
labelId: this.labelId, labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot, hasLabelSlot,
helpText: this.helpText, helpText: this.helpText,
helpTextId: this.helpTextId, helpTextId: this.helpTextId,
hasHelpTextSlot: this.hasHelpTextSlot hasHelpTextSlot
}) })
)} )}
@input=${this.handleInput} @input=${this.handleInput}

Wyświetl plik

@ -6,7 +6,7 @@ import { emit } from '../../internal/event';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { getLabelledBy, renderFormControl } from '../../internal/form-control'; import { getLabelledBy, renderFormControl } from '../../internal/form-control';
import { getTextContent } from '../../internal/slot'; import { getTextContent } from '../../internal/slot';
import { hasSlot } from '../../internal/slot'; import { HasSlotController } from '../../internal/slot';
import type SlDropdown from '../dropdown/dropdown'; import type SlDropdown from '../dropdown/dropdown';
import type SlIconButton from '../icon-button/icon-button'; import type SlIconButton from '../icon-button/icon-button';
import type SlMenu from '../menu/menu'; import type SlMenu from '../menu/menu';
@ -65,14 +65,13 @@ export default class SlSelect extends LitElement {
@query('.select__hidden-select') input: HTMLInputElement; @query('.select__hidden-select') input: HTMLInputElement;
@query('.select__menu') menu: SlMenu; @query('.select__menu') menu: SlMenu;
private hasSlotController = new HasSlotController(this, ['help-text', 'label']);
private inputId = `select-${++id}`; private inputId = `select-${++id}`;
private helpTextId = `select-help-text-${id}`; private helpTextId = `select-help-text-${id}`;
private labelId = `select-label-${id}`; private labelId = `select-label-${id}`;
private resizeObserver: ResizeObserver; private resizeObserver: ResizeObserver;
@state() private hasFocus = false; @state() private hasFocus = false;
@state() private hasHelpTextSlot = false;
@state() private hasLabelSlot = false;
@state() private isOpen = false; @state() private isOpen = false;
@state() private displayLabel = ''; @state() private displayLabel = '';
@state() private displayTags: TemplateResult[] = []; @state() private displayTags: TemplateResult[] = [];
@ -130,12 +129,11 @@ export default class SlSelect extends LitElement {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.handleSlotChange = this.handleSlotChange.bind(this); this.handleMenuSlotChange = this.handleMenuSlotChange.bind(this);
this.resizeObserver = new ResizeObserver(() => this.resizeMenu()); this.resizeObserver = new ResizeObserver(() => this.resizeMenu());
this.updateComplete.then(() => { this.updateComplete.then(() => {
this.resizeObserver.observe(this); this.resizeObserver.observe(this);
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
this.syncItemsFromValue(); this.syncItemsFromValue();
}); });
} }
@ -147,7 +145,6 @@ export default class SlSelect extends LitElement {
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this.resizeObserver.unobserve(this); 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. */ /** 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(); this.syncItemsFromValue();
} }
@watch('helpText') async handleMenuSlotChange() {
@watch('label')
async handleSlotChange() {
this.hasHelpTextSlot = hasSlot(this, 'help-text');
this.hasLabelSlot = hasSlot(this, 'label');
// Wait for items to render before gathering labels otherwise the slot won't exist // Wait for items to render before gathering labels otherwise the slot won't exist
const items = this.getItems(); const items = this.getItems();
@ -437,6 +429,8 @@ export default class SlSelect extends LitElement {
} }
render() { render() {
const hasLabelSlot = this.hasSlotController.test('label');
const hasHelpTextSlot = this.hasSlotController.test('help-text');
const hasSelection = this.multiple ? this.value?.length > 0 : this.value !== ''; const hasSelection = this.multiple ? this.value?.length > 0 : this.value !== '';
return renderFormControl( return renderFormControl(
@ -444,10 +438,10 @@ export default class SlSelect extends LitElement {
inputId: this.inputId, inputId: this.inputId,
label: this.label, label: this.label,
labelId: this.labelId, labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot, hasLabelSlot,
helpTextId: this.helpTextId, helpTextId: this.helpTextId,
helpText: this.helpText, helpText: this.helpText,
hasHelpTextSlot: this.hasHelpTextSlot, hasHelpTextSlot,
size: this.size, size: this.size,
onLabelClick: () => this.handleLabelClick() onLabelClick: () => this.handleLabelClick()
}, },
@ -489,10 +483,10 @@ export default class SlSelect extends LitElement {
getLabelledBy({ getLabelledBy({
label: this.label, label: this.label,
labelId: this.labelId, labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot, hasLabelSlot,
helpText: this.helpText, helpText: this.helpText,
helpTextId: this.helpTextId, helpTextId: this.helpTextId,
hasHelpTextSlot: this.hasHelpTextSlot hasHelpTextSlot
}) })
)} )}
aria-haspopup="true" aria-haspopup="true"
@ -548,7 +542,7 @@ export default class SlSelect extends LitElement {
</div> </div>
<sl-menu part="menu" class="select__menu" @sl-select=${this.handleMenuSelect}> <sl-menu part="menu" class="select__menu" @sl-select=${this.handleMenuSelect}>
<slot @slotchange=${this.handleSlotChange}></slot> <slot @slotchange=${this.handleMenuSlotChange}></slot>
</sl-menu> </sl-menu>
</sl-dropdown> </sl-dropdown>
` `

Wyświetl plik

@ -6,7 +6,7 @@ import { emit } from '../../internal/event';
import { live } from 'lit/directives/live.js'; import { live } from 'lit/directives/live.js';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { getLabelledBy, renderFormControl } from '../../internal/form-control'; import { getLabelledBy, renderFormControl } from '../../internal/form-control';
import { hasSlot } from '../../internal/slot'; import { HasSlotController } from '../../internal/slot';
import styles from './textarea.styles'; import styles from './textarea.styles';
let id = 0; let id = 0;
@ -35,14 +35,13 @@ export default class SlTextarea extends LitElement {
@query('.textarea__control') input: HTMLTextAreaElement; @query('.textarea__control') input: HTMLTextAreaElement;
private hasSlotController = new HasSlotController(this, ['help-text', 'label']);
private inputId = `textarea-${++id}`; private inputId = `textarea-${++id}`;
private helpTextId = `textarea-help-text-${id}`; private helpTextId = `textarea-help-text-${id}`;
private labelId = `textarea-label-${id}`; private labelId = `textarea-label-${id}`;
private resizeObserver: ResizeObserver; private resizeObserver: ResizeObserver;
@state() private hasFocus = false; @state() private hasFocus = false;
@state() private hasHelpTextSlot = false;
@state() private hasLabelSlot = false;
/** The textarea's size. */ /** The textarea's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium'; @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
@ -115,10 +114,7 @@ export default class SlTextarea extends LitElement {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.handleSlotChange = this.handleSlotChange.bind(this);
this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight()); this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight());
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
this.handleSlotChange();
this.updateComplete.then(() => { this.updateComplete.then(() => {
this.setTextareaHeight(); this.setTextareaHeight();
@ -133,7 +129,6 @@ export default class SlTextarea extends LitElement {
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this.resizeObserver.unobserve(this.input); this.resizeObserver.unobserve(this.input);
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
} }
/** Sets focus on the textarea. */ /** Sets focus on the textarea. */
@ -243,13 +238,6 @@ export default class SlTextarea extends LitElement {
this.setTextareaHeight(); this.setTextareaHeight();
} }
@watch('helpText')
@watch('label')
handleSlotChange() {
this.hasHelpTextSlot = hasSlot(this, 'help-text');
this.hasLabelSlot = hasSlot(this, 'label');
}
@watch('value') @watch('value')
handleValueChange() { handleValueChange() {
if (this.input) { if (this.input) {
@ -269,15 +257,18 @@ export default class SlTextarea extends LitElement {
} }
render() { render() {
const hasLabelSlot = this.hasSlotController.test('label');
const hasHelpTextSlot = this.hasSlotController.test('help-text');
return renderFormControl( return renderFormControl(
{ {
inputId: this.inputId, inputId: this.inputId,
label: this.label, label: this.label,
labelId: this.labelId, labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot, hasLabelSlot,
helpTextId: this.helpTextId, helpTextId: this.helpTextId,
helpText: this.helpText, helpText: this.helpText,
hasHelpTextSlot: this.hasHelpTextSlot, hasHelpTextSlot,
size: this.size size: this.size
}, },
html` html`
@ -321,10 +312,10 @@ export default class SlTextarea extends LitElement {
getLabelledBy({ getLabelledBy({
label: this.label, label: this.label,
labelId: this.labelId, labelId: this.labelId,
hasLabelSlot: this.hasLabelSlot, hasLabelSlot,
helpText: this.helpText, helpText: this.helpText,
helpTextId: this.helpTextId, helpTextId: this.helpTextId,
hasHelpTextSlot: this.hasHelpTextSlot hasHelpTextSlot
}) })
)} )}
@change=${this.handleChange} @change=${this.handleChange}

Wyświetl plik

@ -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 // 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. // 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; 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;
});
}