shoelace/src/components/tree-item/tree-item.ts

307 wiersze
10 KiB
TypeScript
Czysty Zwykły widok Historia

2023-01-13 20:43:55 +00:00
import '../checkbox/checkbox';
import '../icon/icon';
import '../spinner/spinner';
import { animateTo, shimKeyframesHeightAuto, stopAnimations } from '../../internal/animate';
import { classMap } from 'lit/directives/class-map.js';
2023-01-13 20:43:55 +00:00
import { customElement, property, query, state } from 'lit/decorators.js';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import { html } from 'lit';
import { live } from 'lit/directives/live.js';
2023-01-13 20:43:55 +00:00
import { LocalizeController } from '../../utilities/localize';
import { watch } from '../../internal/watch';
import { when } from 'lit/directives/when.js';
2022-08-17 15:37:37 +00:00
import ShoelaceElement from '../../internal/shoelace-element';
import styles from './tree-item.styles';
2022-08-17 20:31:23 +00:00
import type { CSSResultGroup, PropertyValueMap } from 'lit';
/**
Enrich components `@summary` with description from docs (#962) * keep header styles with repositioned description text * `animated-image` move description to component * code style * `avatar` add summary from docs * `badge` add summary from docs * `breadcrumb` add summary from docs * `button` add summary from docs * lead sentence is now part of the header * `button-group` add summary from docs * `card` add summary from docs * `checkbox` add summary from docs * `color-picker` add summary from docs * `details` add summary from docs * `dialog` add summary from docs * `divider` add summary from docs * `drawer` add summary from docs * `dropdown` add summary from docs * `format-bytes` add summary from docs * `format-date` add summary from docs * `format-number` add summary from docs * `icon` add summary from docs * `icon-button` add summary from docs * `image-comparer` add summary from docs * `include` add summary from docs * `input` add summary from docs * `menu` add summary from docs * `menu-item` add summary from docs * `menu-label` add summary from docs * `popup` add summary from docs * `progressbar` add summary from docs * `progress-ring` add summary from docs * `radio` add summary from docs * `radio-button` add summary from docs * `range` add summary from docs * `rating` add summary from docs * `relative-time` add summary from docs * `select` add summary from docs * `skeleton` add summary from docs * `spinner` add summary from docs * `split-panel` add summary from docs * `switch` add summary from docs * `tab-group` add summary from docs * `tag` add summary from docs * `textarea` add summary from docs * `tooltip` add summary from docs * `visually-hidden` add summary from docs * `animation` add summary from docs * `breadcrumb-item` add summary from docs * `mutation-observer` add summary from docs * `radio-group` add summary from docs * `resize-observer` add summary from docs * `tab` add summary from docs * `tab-panel` add summary from docs * `tree` add summary from docs * `tree-item` add summary from docs * remove `title` for further usage of `Sl` classnames in docs * revert: use markdown parser for component summary
2022-10-21 13:56:35 +00:00
* @summary A tree item serves as a hierarchical node that lives inside a [tree](/components/tree).
2023-01-12 15:26:25 +00:00
* @documentation https://shoelace.style/components/tree-item
2022-12-20 18:36:06 +00:00
* @status stable
2023-01-12 15:26:25 +00:00
* @since 2.0
*
* @dependency sl-checkbox
2022-07-26 19:53:24 +00:00
* @dependency sl-icon
* @dependency sl-spinner
*
2022-12-06 16:18:14 +00:00
* @event sl-expand - Emitted when the tree item expands.
* @event sl-after-expand - Emitted after the tree item expands and all animations are complete.
* @event sl-collapse - Emitted when the tree item collapses.
* @event sl-after-collapse - Emitted after the tree item collapses and all animations are complete.
* @event sl-lazy-change - Emitted when the tree item's lazy state changes.
2022-08-26 14:28:18 +00:00
* @event sl-lazy-load - Emitted when a lazy item is selected. Use this event to asynchronously load data and append
* items to the tree before expanding. After appending new items, remove the `lazy` attribute to remove the loading
* state and update the tree.
*
* @slot - The default slot.
2022-12-06 16:18:14 +00:00
* @slot expand-icon - The icon to show when the tree item is expanded.
* @slot collapse-icon - The icon to show when the tree item is collapsed.
*
2022-12-06 16:18:14 +00:00
* @csspart base - The component's base wrapper.
* @csspart item - The tree item's container. This element wraps everything except slotted tree item children.
* @csspart item--disabled - Applied when the tree item is disabled.
* @csspart item--expanded - Applied when the tree item is expanded.
2022-07-26 19:53:24 +00:00
* @csspart item--indeterminate - Applied when the selection is indeterminate.
2022-12-06 16:18:14 +00:00
* @csspart item--selected - Applied when the tree item is selected.
* @csspart indentation - The tree item's indentation container.
* @csspart expand-button - The container that wraps the tree item's expand button and spinner.
* @csspart label - The tree item's label.
* @csspart children - The container that wraps the tree item's nested children.
*/
@customElement('sl-tree-item')
2022-08-17 15:37:37 +00:00
export default class SlTreeItem extends ShoelaceElement {
2022-08-17 20:31:23 +00:00
static styles: CSSResultGroup = styles;
2022-12-06 16:37:44 +00:00
static isTreeItem(node: Node) {
return node instanceof Element && node.getAttribute('role') === 'treeitem';
}
private readonly localize = new LocalizeController(this);
2022-07-26 19:53:24 +00:00
@state() indeterminate = false;
@state() isLeaf = false;
@state() loading = false;
@state() selectable = false;
/** Expands the tree item. */
@property({ type: Boolean, reflect: true }) expanded = false;
2022-07-26 19:53:24 +00:00
/** Draws the tree item in a selected state. */
@property({ type: Boolean, reflect: true }) selected = false;
2022-07-26 19:53:24 +00:00
/** Disables the tree item. */
@property({ type: Boolean, reflect: true }) disabled = false;
2022-07-26 19:53:24 +00:00
/** Enables lazy loading behavior. */
@property({ type: Boolean, reflect: true }) lazy = false;
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
@query('slot[name=children]') childrenSlot: HTMLSlotElement;
@query('.tree-item__item') itemElement: HTMLDivElement;
@query('.tree-item__children') childrenContainer: HTMLDivElement;
2022-08-30 22:51:51 +00:00
@query('.tree-item__expand-button slot') expandButtonSlot: HTMLSlotElement;
2023-01-03 18:36:12 +00:00
connectedCallback() {
super.connectedCallback();
this.setAttribute('role', 'treeitem');
this.setAttribute('tabindex', '-1');
if (this.isNestedItem()) {
this.slot = 'children';
}
}
firstUpdated() {
this.childrenContainer.hidden = !this.expanded;
this.childrenContainer.style.height = this.expanded ? 'auto' : '0';
this.isLeaf = !this.lazy && this.getChildrenItems().length === 0;
this.handleExpandedChange();
}
2023-01-03 20:04:07 +00:00
private async animateCollapse() {
this.emit('sl-collapse');
await stopAnimations(this.childrenContainer);
const { keyframes, options } = getAnimation(this, 'tree-item.collapse', { dir: this.localize.dir() });
await animateTo(
this.childrenContainer,
shimKeyframesHeightAuto(keyframes, this.childrenContainer.scrollHeight),
options
);
this.childrenContainer.hidden = true;
this.emit('sl-after-collapse');
}
// Checks whether the item is nested into an item
private isNestedItem(): boolean {
const parent = this.parentElement;
return !!parent && SlTreeItem.isTreeItem(parent);
}
private handleChildrenSlotChange() {
this.loading = false;
this.isLeaf = !this.lazy && this.getChildrenItems().length === 0;
}
protected willUpdate(changedProperties: PropertyValueMap<SlTreeItem> | Map<PropertyKey, unknown>) {
if (changedProperties.has('selected') && !changedProperties.has('indeterminate')) {
this.indeterminate = false;
}
}
private async animateExpand() {
this.emit('sl-expand');
await stopAnimations(this.childrenContainer);
this.childrenContainer.hidden = false;
const { keyframes, options } = getAnimation(this, 'tree-item.expand', { dir: this.localize.dir() });
await animateTo(
this.childrenContainer,
shimKeyframesHeightAuto(keyframes, this.childrenContainer.scrollHeight),
options
);
this.childrenContainer.style.height = 'auto';
this.emit('sl-after-expand');
}
@watch('loading', { waitUntilFirstUpdate: true })
handleLoadingChange() {
this.setAttribute('aria-busy', this.loading ? 'true' : 'false');
if (!this.loading) {
this.animateExpand();
}
}
@watch('disabled')
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
@watch('selected')
handleSelectedChange() {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
}
@watch('expanded', { waitUntilFirstUpdate: true })
handleExpandedChange() {
if (!this.isLeaf) {
this.setAttribute('aria-expanded', this.expanded ? 'true' : 'false');
} else {
this.removeAttribute('aria-expanded');
}
}
@watch('expanded', { waitUntilFirstUpdate: true })
handleExpandAnimation() {
if (this.expanded) {
if (this.lazy) {
this.loading = true;
2022-09-16 20:21:40 +00:00
this.emit('sl-lazy-load');
} else {
this.animateExpand();
}
} else {
this.animateCollapse();
}
}
2022-08-26 14:28:18 +00:00
@watch('lazy', { waitUntilFirstUpdate: true })
handleLazyChange() {
2022-09-16 20:21:40 +00:00
this.emit('sl-lazy-change');
2022-08-26 14:28:18 +00:00
}
2023-01-03 20:04:07 +00:00
/** Gets all the nested tree items in this node. */
getChildrenItems({ includeDisabled = true }: { includeDisabled?: boolean } = {}): SlTreeItem[] {
return this.childrenSlot
? ([...this.childrenSlot.assignedElements({ flatten: true })].filter(
2022-12-06 16:37:44 +00:00
(item: SlTreeItem) => SlTreeItem.isTreeItem(item) && (includeDisabled || !item.disabled)
) as SlTreeItem[])
: [];
}
render() {
2022-07-26 19:53:24 +00:00
const isRtl = this.localize.dir() === 'rtl';
const showExpandButton = !this.loading && (!this.isLeaf || this.lazy);
2022-07-26 19:53:24 +00:00
return html`
2022-07-26 19:53:24 +00:00
<div
part="base"
class="${classMap({
'tree-item': true,
2022-08-30 22:51:51 +00:00
'tree-item--expanded': this.expanded,
2022-07-26 19:53:24 +00:00
'tree-item--selected': this.selected,
'tree-item--disabled': this.disabled,
'tree-item--leaf': this.isLeaf,
2022-12-01 21:02:36 +00:00
'tree-item--has-expand-button': showExpandButton,
2022-07-26 19:53:24 +00:00
'tree-item--rtl': this.localize.dir() === 'rtl'
})}"
>
<div
2022-07-26 19:53:24 +00:00
class="tree-item__item"
part="
item
${this.disabled ? 'item--disabled' : ''}
${this.expanded ? 'item--expanded' : ''}
${this.indeterminate ? 'item--indeterminate' : ''}
${this.selected ? 'item--selected' : ''}
"
>
<div class="tree-item__indentation" part="indentation"></div>
2022-07-26 19:53:24 +00:00
<div
2022-09-05 16:56:00 +00:00
part="expand-button"
2022-07-26 19:53:24 +00:00
class=${classMap({
'tree-item__expand-button': true,
'tree-item__expand-button--visible': showExpandButton
2022-07-26 19:53:24 +00:00
})}
aria-hidden="true"
>
${when(this.loading, () => html` <sl-spinner></sl-spinner> `)}
2022-12-01 21:02:36 +00:00
<slot class="tree-item__expand-icon-slot" name="expand-icon">
<sl-icon library="system" name=${isRtl ? 'chevron-left' : 'chevron-right'}></sl-icon>
</slot>
<slot class="tree-item__expand-icon-slot" name="collapse-icon">
<sl-icon library="system" name=${isRtl ? 'chevron-left' : 'chevron-right'}></sl-icon>
</slot>
</div>
${when(
this.selectable,
() =>
html`
<sl-checkbox
tabindex="-1"
class="tree-item__checkbox"
?disabled="${this.disabled}"
?checked="${live(this.selected)}"
?indeterminate="${this.indeterminate}"
>
2022-12-02 22:03:59 +00:00
<slot class="tree-item__label" part="label"></slot>
</sl-checkbox>
`,
2022-12-02 22:03:59 +00:00
() => html` <slot class="tree-item__label" part="label"></slot> `
)}
</div>
2022-12-02 22:03:59 +00:00
<slot
name="children"
class="tree-item__children"
part="children"
role="group"
@slotchange="${this.handleChildrenSlotChange}"
></slot>
</div>
`;
}
}
setDefaultAnimation('tree-item.expand', {
keyframes: [
{ height: '0', opacity: '0', overflow: 'hidden' },
{ height: 'auto', opacity: '1', overflow: 'hidden' }
],
options: { duration: 250, easing: 'cubic-bezier(0.4, 0.0, 0.2, 1)' }
});
setDefaultAnimation('tree-item.collapse', {
keyframes: [
{ height: 'auto', opacity: '1', overflow: 'hidden' },
{ height: '0', opacity: '0', overflow: 'hidden' }
],
options: { duration: 200, easing: 'cubic-bezier(0.4, 0.0, 0.2, 1)' }
});
declare global {
interface HTMLElementTagNameMap {
'sl-tree-item': SlTreeItem;
}
}