From ce09869ea24f9eed7182cd4f4a47136f7882919f Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 26 Jul 2022 15:53:24 -0400 Subject: [PATCH] various updates --- cspell.json | 2 + docs/components/tree-item.md | 186 ++++--- docs/components/tree.md | 484 +++++++++++-------- docs/resources/changelog.md | 1 + src/components/tree-item/tree-item.styles.ts | 59 ++- src/components/tree-item/tree-item.test.ts | 15 +- src/components/tree-item/tree-item.ts | 104 ++-- src/components/tree/tree.styles.ts | 18 +- src/components/tree/tree.test.ts | 115 +---- src/components/tree/tree.ts | 105 ++-- src/internal/string.ts | 8 - src/shoelace.ts | 2 +- 12 files changed, 580 insertions(+), 519 deletions(-) diff --git a/cspell.json b/cspell.json index f36763fe..48394a5c 100644 --- a/cspell.json +++ b/cspell.json @@ -79,6 +79,7 @@ "monospace", "mousedown", "mouseup", + "multiselectable", "nextjs", "nocheck", "noopener", @@ -116,6 +117,7 @@ "textareas", "textfield", "transitionend", + "treeitem", "Triaging", "turbolinks", "unbundles", diff --git a/docs/components/tree-item.md b/docs/components/tree-item.md index 5a3cc753..303b066d 100644 --- a/docs/components/tree-item.md +++ b/docs/components/tree-item.md @@ -2,94 +2,164 @@ [component-header:sl-tree-item] -A tree item is a hierarchical node of a tree. +A tree item serves as a hierarchical node that lives inside a [tree](/components/tree). ```html preview - Tree node + + + Item 1 + Item A + Item B + Item C + + Item 2 + Item 3 + +``` + + +```jsx react +import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + Item 1 + Item A + Item B + Item C + + Item 2 + Item 3 + +); ``` ## Examples ### Nested tree items -A tree item can contain other items, this allow the user to expand or collapse nested nodes accordingly. +A tree item can contain other tree items. This allows the node to be expanded or collapsed by the user. ```html preview - - Parent Node - Child 1 - Child 2 - Child 3 - + + + Item 1 + + Item A + Item Z + Item Y + Item X + + Item B + Item C + + Item 2 + Item 3 + ``` -### Expanded + +```jsx react +import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react'; -Use the `expanded` attribute to display the nested items. - -```html preview - - Parent Node - Child 1 - Child 2 - Child 3 - +const App = () => ( + + + Item 1 + + Item A + Item Z + Item Y + Item X + + Item B + Item C + + Item 2 + Item 3 + +); ``` ### Selected -Use the `selected` attribute to the mark the item as selected. +Use the `selected` attribute to select a tree item initially. ```html preview - - Parent Node - Child 1 - Child 2 - Child 3 - + + + Item 1 + Item A + Item B + Item C + + Item 2 + Item 3 + ``` -### Selectable + +```jsx react +import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react'; -Use the `selectable` attribute to display the checkbox. - -```html preview - - Parent Node - Child 1 - Child 2 - Child 3 - - - +const App = () => ( + + + Item 1 + Item A + Item B + Item C + + Item 2 + Item 3 + +); ``` -### Lazy +### Expanded -Use the `lazy` to specify that the content is not yet loaded. When the user tries to expand the node, -a `sl-lazy-load` event is emitted. +Use the `expanded` attribute to expand a tree item initially. ```html preview - Parent Node + + + Item 1 + + Item A + Item Z + Item Y + Item X + + Item B + Item C + + Item 2 + Item 3 + ``` -### Indentation size + +```jsx react +import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react'; -Use the `--indentation-size` custom property to set the tree item's indentation. - -```html preview - - Parent Node - Child 1 - Child 2 - Child 3 - +const App = () => ( + + + Item 1 + + Item A + Item Z + Item Y + Item X + + Item B + Item C + + Item 2 + Item 3 + +); ``` [component-metadata:sl-tree-item] diff --git a/docs/components/tree.md b/docs/components/tree.md index bf7afcb9..7024c3eb 100644 --- a/docs/components/tree.md +++ b/docs/components/tree.md @@ -2,181 +2,259 @@ [component-header:sl-tree] -A tree component allow the user to display a hierarchical list of items, expanding and collapsing the nodes that have nested items. -The user can select one or more items from the list. +Trees allow you to display a hierarchical list of selectable [tree items](/components/tree-item). Items with children can be expanded and collapsed as desired by the user. ```html preview - - Getting Started + + Deciduous + Birch - Overview - Quick Start - New to Web Components? - What Problem Does This Solve? - Browser Support - License - Attribution + Maple + Field maple + Red maple + Sugar maple - Installation - Usage + Oak - Frameworks - React - Vue - Angular + Coniferous + Cedar + Pine + Spruce - Resources + + Non-trees + Bamboo + Cactus + Fern + ``` + ```jsx react import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react'; const App = () => ( - - Getting Started - Overview - Installation - Usage + + Deciduous + Birch + + Maple + Field maple + Red maple + Sugar maple + + Oak - Frameworks - React - Vue - Angular + Coniferous + Cedar + Pine + Spruce - Resources + + Non-trees + Bamboo + Cactus + Fern + ); ``` ## Examples -### Selection modes +### Selection Modes -Use the `selection` attribute to specify the selection behavior of the tree +Use the `selection` attribute to change the selection behavior of the tree. -- Set `none` (_default_) to disable the selection. -- Set `single` to allow the selection of a single item. -- Set `leaf` to allow the selection of a single leaf node. Clicking on a parent node will expand/collapse the node. +- Set `single` to allow the selection of a single item (default). - Set `multiple` to allow the selection of multiple items. +- Set `leaf` to allow the selection of a single leaf node. Clicking on a parent node will expand/collapse the node. ```html preview - - none - single - leaf - multiple + + Single + Multiple + Leaf -
- - - Parent - - Parent 1 - Child 1 - Child 2 - +
+ + + + Item 1 - Parent 2 - Child 1 - Child 2 - Child 3 + Item A + Item Z + Item Y + Item X + Item B + Item C + Item 2 + Item 3 - + ``` + ```jsx react import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react'; const App = () => { - const [selection, setSelection] = useState('none'); + const [selection, setSelection] = useState('single'); return ( <> setSelection(event.target.value)}> - none single - leaf multiple + leaf +
- - - Parent - - Parent 1 Child 1 - Child 2 - + + + + Item 1 - Parent 2 Child 1 - Child 2 - Child 3 + Item A + Item Z + Item Y + Item X + Item B + Item C + Item 2 + Item 3 ); }; ``` -### Lazy loading +### Showing Indent Guides -Use the `lazy` attribute on a item to indicate that the content is not yet present and will be loaded later. -When the user tries to expand the node, the `loading` state is set to `true` and a special event named -`sl-lazy-load` is emitted to let the loading of the content. The item will remain in a loading state until its content -is changed. - -If you want to disable this behavior, for example after the content has been loaded, it will be sufficient to set -`lazy` to `false`. +Indent guides can be drawn by setting `--indent-guide-width`. You can also change the color, offset, and style, using `--indent-guide-color`, `--indent-guide-style`, and `--indent-guide-offset`, respectively. ```html preview - - Getting Started + + + Deciduous + Birch + + Maple + Field maple + Red maple + Sugar maple + + Oak + + + + Coniferous + Cedar + Pine + Spruce + + + + Non-trees + Bamboo + Cactus + Fern + + + + +``` + + +```jsx react +import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + + + Deciduous + Birch + + Maple + Field maple + Red maple + Sugar maple + + Oak + + + + Coniferous + Cedar + Pine + Spruce + + + + Non-trees + Bamboo + Cactus + Fern + + +); +``` + +### Lazy Loading + +Use the `lazy` attribute on a tree item to indicate that the content is not yet present and will be loaded later. When the user tries to expand the node, the `loading` state is set to `true` and the `sl-lazy-load` event will be emitted to allow you to load data asynchronously. The item will remain in a loading state until its content is changed. + +If you want to disable this behavior after the first load, simply remove the `lazy` attribute and, on the next expand, the existing content will be shown instead. + +```html preview + + Available Trees ``` @@ -191,17 +269,17 @@ const App = () => { const handleLazyLoad = () => { // Simulate asynchronous loading setTimeout(() => { - setChildItems(['Overview', 'Installation', 'Usage']); + setChildItems(['Birch', 'Cedar', 'Maple', 'Pine']); - // Disable lazy mode since the content has been loaded + // Disable lazy mode once the content has been loaded setLazy(false); - }, 2000); + }, 1000); }; return ( - + - Getting Started + Available Trees {childItems.map(item => ( {item} ))} @@ -211,133 +289,109 @@ const App = () => { }; ``` -### Styling trees +### With Icons -Using CSS parts is possible to apply custom styles to the tree. -For example, it is possible to change the hover effect and to highlight the selected item. +Decorative icons can be used before labels to provide hints for each node. ```html preview - - + - Getting Started + + Root + - Overview - Quick Start - New to Web Components? - What Problem Does This Solve? - Browser Support - License - Attribution + + Folder 1 + + + File 1 - 1 + + + + File 1 - 2 + + + + File 1 - 3 + - Installation - Usage - - - Frameworks - React - Vue - Angular - - - Resources - -``` - -### With indentation lines - -```html preview - - - - Getting Started - - Overview - Quick Start - New to Web Components? - What Problem Does This Solve? - Browser Support - License - Attribution - - Installation - Usage - - - - Frameworks - React - Vue - Angular - - - Resources - -``` - -### With icons - -```html preview - - - - Root - - Folder 1 - File 1 - 1 - File 1 - 2 - File 1 - 3 - Folder 2 - File 2 - 1 - File 2 - 2 + + + File 2 - 1 + + + + File 2 - 2 + + + + + File 1 - File 1 ``` +```jsx react +import { SlIcon, SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react'; + +const App = () => { + const [childItems, setChildItems] = useState([]); + const [lazy, setLazy] = useState(true); + + const handleLazyLoad = () => { + // Simulate asynchronous loading + setTimeout(() => { + setChildItems(['Overview', 'Installation', 'Usage']); + + // Disable lazy mode once the content has been loaded + setLazy(false); + }, 1000); + }; + + return ( + + + + Root + + + Folder 1 + + File 1 - 1 + + + + File 1 - 2 + + + + File 1 - 3 + + + + + Folder 2 + + File 2 - 1 + + + + File 2 - 2 + + + + + File 1 + + + + ); +}; +``` + [component-metadata:sl-tree] diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index c4ee59b8..7e81e6fd 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -10,6 +10,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis ## Next +- Added experimental `` and `` components [#823](https://github.com/shoelace-style/shoelace/pull/823) - Added `--indicator-width` custom property to `` [#837](https://github.com/shoelace-style/shoelace/issues/837) - Added Swedish translation [#838](https://github.com/shoelace-style/shoelace/pull/838) - Changed the type of component styles from `CSSResult` to `CSSResultGroup` [#828](https://github.com/shoelace-style/shoelace/issues/828) diff --git a/src/components/tree-item/tree-item.styles.ts b/src/components/tree-item/tree-item.styles.ts index e4c0c750..fe3d6963 100644 --- a/src/components/tree-item/tree-item.styles.ts +++ b/src/components/tree-item/tree-item.styles.ts @@ -7,23 +7,25 @@ export default css` :host { display: block; outline: 0; + z-index: 0; } :host(:focus) { outline: 0; } + ::slotted(sl-icon) { + margin-inline-end: var(--sl-spacing-x-small); + } + .tree-item { position: relative; display: flex; align-items: stretch; flex-direction: column; - color: var(--sl-color-neutral-700); - user-select: none; white-space: nowrap; - cursor: pointer; } .tree-item__checkbox { @@ -56,33 +58,46 @@ export default css` align-items: center; justify-content: center; box-sizing: content-box; - color: var(--sl-color-neutral-400); + color: var(--sl-color-neutral-500); padding: var(--sl-spacing-x-small); width: 1rem; height: 1rem; + cursor: pointer; + } + + .tree-item__expand-button--visible { + cursor: pointer; } .tree-item__item { display: flex; align-items: center; - cursor: pointer; + border-inline-start: solid 3px transparent; } - .tree-item__item--disabled { - color: var(--sl-color-neutral-400); + .tree-item--disabled .tree-item__item { + opacity: 0.5; outline: none; cursor: not-allowed; } + :host(:not([aria-disabled='true'])) .tree-item__item:hover { + background-color: var(--sl-color-neutral-100); + } + :host(:not([aria-disabled='true']):focus-visible) .tree-item__item { outline: var(--sl-focus-ring); outline-offset: var(--sl-focus-ring-offset); + z-index: 2; } - :host(:not([aria-disabled='true'])) .tree-item__item--selected, - :host(:not([aria-disabled='true'])) .tree-item__item:hover, - :host(:not([aria-disabled='true'])) .tree-item__item:hover sl-checkbox::part(label) { - color: var(--sl-color-primary-600); + :host(:not([aria-disabled='true'])) .tree-item--selected .tree-item__item { + background-color: var(--sl-color-neutral-100); + border-inline-start-color: var(--sl-color-primary-600); + } + + :host(:not([aria-disabled='true'])) .tree-item__expand-button { + color: var(--sl-color-neutral-600); } .tree-item__label { @@ -92,6 +107,26 @@ export default css` } .tree-item__children { - font-size: calc(1em + var(--indentation-size, var(--sl-spacing-medium))); + font-size: calc(1em + var(--indent-size, var(--sl-spacing-medium))); + } + + /* Indentation lines */ + .tree-item__children { + position: relative; + } + + .tree-item__children::before { + content: ''; + position: absolute; + top: var(--indent-guide-offset); + bottom: var(--indent-guide-offset); + left: calc(1em - (var(--indent-guide-width) / 2) - 1px); + border-inline-end: var(--indent-guide-width) var(--indent-guide-style) var(--indent-guide-color); + z-index: 1; + } + + .tree-item--rtl .tree-item__children::before { + left: auto; + right: 1em; } `; diff --git a/src/components/tree-item/tree-item.test.ts b/src/components/tree-item/tree-item.test.ts index 894777fd..3f26d514 100644 --- a/src/components/tree-item/tree-item.test.ts +++ b/src/components/tree-item/tree-item.test.ts @@ -26,7 +26,7 @@ describe('', () => { expect(leafItem).to.have.attribute('aria-disabled', 'false'); }); - describe('when contain child tree items', () => { + describe('when it contains child tree items', () => { it('should set isLeaf to false', () => { // Assert expect(parentItem.isLeaf).to.be.false; @@ -156,7 +156,7 @@ describe('', () => { }); }); - describe('when a item is disabled', () => { + describe('when the item is disabled', () => { it('should update the aria-disabled attribute', async () => { // Act leafItem.disabled = true; @@ -175,4 +175,15 @@ describe('', () => { expect(leafItem.shadowRoot?.querySelector('.tree-item__item')?.part.contains('item--disabled')).to.be.true; }); }); + + describe('when the item is expanded', () => { + it('should set item--expanded part', async () => { + // Act + leafItem.expanded = true; + await leafItem.updateComplete; + + // Assert + expect(leafItem.shadowRoot?.querySelector('.tree-item__item')?.part.contains('item--expanded')).to.be.true; + }); + }); }); diff --git a/src/components/tree-item/tree-item.ts b/src/components/tree-item/tree-item.ts index f60f58d4..6fab18b9 100644 --- a/src/components/tree-item/tree-item.ts +++ b/src/components/tree-item/tree-item.ts @@ -1,16 +1,16 @@ -import { LocalizeController } from '@shoelace-style/localize'; import { LitElement, html } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { live } from 'lit/directives/live.js'; import { when } from 'lit/directives/when.js'; import { animateTo, shimKeyframesHeightAuto, stopAnimations } from 'src/internal/animate'; -import { stringMap } from 'src/internal/string'; import { getAnimation, setDefaultAnimation } from 'src/utilities/animation-registry'; import '../../components/checkbox/checkbox'; +import '../../components/icon/icon'; import '../../components/spinner/spinner'; import { emit } from '../../internal/event'; import { watch } from '../../internal/watch'; +import { LocalizeController } from '../../utilities/localize'; import styles from './tree-item.styles'; import type { PropertyValueMap } from 'lit'; @@ -23,6 +23,7 @@ export function isTreeItem(element: Element) { * @status experimental * * @dependency sl-checkbox + * @dependency sl-icon * @dependency sl-spinner * * @event sl-expand - Emitted when the item expands. @@ -35,13 +36,13 @@ export function isTreeItem(element: Element) { * * @csspart base - The component's internal wrapper. * @csspart item - The item main container. - * @csspart item--selected - The `selected` state of the main container. - * @csspart item--disabled - The `disabled` state of the main container. - * @csspart indentation - The item indentation. - * @csspart label - The item label. - * @csspart children - The item children container. - * - * @cssproperty --indentation-size - The size of the indentation for nested items. (Default: --sl-spacing-medium) + * @csspart item--disabled - Applied when the item is disabled. + * @csspart item--expanded - Applied when the item is expanded. + * @csspart item--indeterminate - Applied when the selection is indeterminate. + * @csspart item--selected - Applied when the item is selected. + * @csspart indentation - The item's indentation container. + * @csspart label - The item's label. + * @csspart children - The item's children container. */ @customElement('sl-tree-item') export default class SlTreeItem extends LitElement { @@ -49,30 +50,23 @@ export default class SlTreeItem extends LitElement { private readonly localize = new LocalizeController(this); - /** Expands the item when is set */ + @state() indeterminate = false; + @state() isLeaf = false; + @state() loading = false; + @state() selectable = false; + + /** Expands the tree item. */ @property({ type: Boolean, reflect: true }) expanded = false; - /** Sets the treeitem's selected state */ + /** Draws the tree item in a selected state. */ @property({ type: Boolean, reflect: true }) selected = false; - /** Disables the treeitem */ + /** Disables the tree item. */ @property({ type: Boolean, reflect: true }) disabled = false; - /** When set, enables the lazy mode behavior */ + /** Enables lazy loading behavior. */ @property({ type: Boolean, reflect: true }) lazy = false; - /** Shows the checkbox when set */ - @property({ type: Boolean }) selectable = false; - - /** Draws the checkbox in a indeterminate state. */ - @state() indeterminate = false; - - /** Specifies whether the node has children nodes */ - @state() isLeaf = false; - - /** Draws the expand button in a loading state. */ - @state() loading = false; - @query('slot:not([name])') defaultSlot: HTMLSlotElement; @query('slot[name=children]') childrenSlot: HTMLSlotElement; @query('.tree-item__item') itemElement: HTMLDivElement; @@ -173,9 +167,7 @@ export default class SlTreeItem extends LitElement { emit(this, 'sl-after-collapse'); } - /** - * @internal Gets all the nested tree items - */ + // Gets all the nested tree items getChildrenItems({ includeDisabled = true }: { includeDisabled?: boolean } = {}): SlTreeItem[] { return this.childrenSlot ? ([...this.childrenSlot.assignedElements({ flatten: true })].filter( @@ -184,17 +176,15 @@ export default class SlTreeItem extends LitElement { : []; } - /** - * @internal Checks whether the item is nested into an item - */ + // Checks whether the item is nested into an item private isNestedItem(): boolean { const parent = this.parentElement; return !!parent && isTreeItem(parent); } - handleToggleExpand(e: Event) { - e.preventDefault(); - e.stopImmediatePropagation(); + handleToggleExpand(event: Event) { + event.preventDefault(); + event.stopImmediatePropagation(); if (!this.disabled) { this.expanded = !this.expanded; @@ -203,7 +193,6 @@ export default class SlTreeItem extends LitElement { handleChildrenSlotChange() { this.loading = false; - this.isLeaf = this.getChildrenItems().length === 0; } @@ -214,28 +203,47 @@ export default class SlTreeItem extends LitElement { } render() { + const isRtl = this.localize.dir() === 'rtl'; + return html` -
+
-