kopia lustrzana https://github.com/shoelace-style/shoelace
various updates
rodzic
33587f51d3
commit
ce09869ea2
|
@ -79,6 +79,7 @@
|
|||
"monospace",
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"multiselectable",
|
||||
"nextjs",
|
||||
"nocheck",
|
||||
"noopener",
|
||||
|
@ -116,6 +117,7 @@
|
|||
"textareas",
|
||||
"textfield",
|
||||
"transitionend",
|
||||
"treeitem",
|
||||
"Triaging",
|
||||
"turbolinks",
|
||||
"unbundles",
|
||||
|
|
|
@ -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
|
||||
<sl-tree-item> Tree node </sl-tree-item>
|
||||
<sl-tree>
|
||||
<sl-tree-item>
|
||||
Item 1
|
||||
<sl-tree-item>Item A</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem>
|
||||
Item 1
|
||||
<SlTreeItem>Item A</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
## 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
|
||||
<sl-tree-item>
|
||||
Parent Node
|
||||
<sl-tree-item> Child 1 </sl-tree-item>
|
||||
<sl-tree-item> Child 2 </sl-tree-item>
|
||||
<sl-tree-item> Child 3 </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree>
|
||||
<sl-tree-item>
|
||||
Item 1
|
||||
<sl-tree-item>
|
||||
Item A
|
||||
<sl-tree-item>Item Z</sl-tree-item>
|
||||
<sl-tree-item>Item Y</sl-tree-item>
|
||||
<sl-tree-item>Item X</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
### Expanded
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
Use the `expanded` attribute to display the nested items.
|
||||
|
||||
```html preview
|
||||
<sl-tree-item expanded>
|
||||
Parent Node
|
||||
<sl-tree-item> Child 1 </sl-tree-item>
|
||||
<sl-tree-item> Child 2 </sl-tree-item>
|
||||
<sl-tree-item> Child 3 </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem>
|
||||
Item 1
|
||||
<SlTreeItem>
|
||||
Item A
|
||||
<SlTreeItem>Item Z</SlTreeItem>
|
||||
<SlTreeItem>Item Y</SlTreeItem>
|
||||
<SlTreeItem>Item X</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
### Selected
|
||||
|
||||
Use the `selected` attribute to the mark the item as selected.
|
||||
Use the `selected` attribute to select a tree item initially.
|
||||
|
||||
```html preview
|
||||
<sl-tree-item expanded>
|
||||
Parent Node
|
||||
<sl-tree-item> Child 1 </sl-tree-item>
|
||||
<sl-tree-item selected> Child 2 </sl-tree-item>
|
||||
<sl-tree-item> Child 3 </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree>
|
||||
<sl-tree-item selected>
|
||||
Item 1
|
||||
<sl-tree-item>Item A</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
### Selectable
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
Use the `selectable` attribute to display the checkbox.
|
||||
|
||||
```html preview
|
||||
<sl-tree-item class="selectable" selectable>
|
||||
Parent Node
|
||||
<sl-tree-item selectable> Child 1 </sl-tree-item>
|
||||
<sl-tree-item selectable> Child 2 </sl-tree-item>
|
||||
<sl-tree-item selectable> Child 3 </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<script>
|
||||
document.querySelector('sl-tree-item.selectable').addEventListener('click', ({ target }) => {
|
||||
if (target.hasAttribute('selectable')) {
|
||||
target.selected = !target.selected;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem selected>
|
||||
Item 1
|
||||
<SlTreeItem>Item A</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
### 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
|
||||
<sl-tree-item lazy> Parent Node </sl-tree-item>
|
||||
<sl-tree>
|
||||
<sl-tree-item expanded>
|
||||
Item 1
|
||||
<sl-tree-item expanded>
|
||||
Item A
|
||||
<sl-tree-item>Item Z</sl-tree-item>
|
||||
<sl-tree-item>Item Y</sl-tree-item>
|
||||
<sl-tree-item>Item X</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
### Indentation size
|
||||
<!-- prettier-ignore -->
|
||||
```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
|
||||
<sl-tree-item style="--indentation-size: 3rem" expanded>
|
||||
Parent Node
|
||||
<sl-tree-item> Child 1 </sl-tree-item>
|
||||
<sl-tree-item> Child 2 </sl-tree-item>
|
||||
<sl-tree-item> Child 3 </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem expanded>
|
||||
Item 1
|
||||
<SlTreeItem expanded>
|
||||
Item A
|
||||
<SlTreeItem>Item Z</SlTreeItem>
|
||||
<SlTreeItem>Item Y</SlTreeItem>
|
||||
<SlTreeItem>Item X</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-tree-item]
|
||||
|
|
|
@ -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
|
||||
<sl-tree>
|
||||
<sl-tree-item expanded>
|
||||
Getting Started
|
||||
<sl-tree-item>
|
||||
Deciduous
|
||||
<sl-tree-item>Birch</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
Overview
|
||||
<sl-tree-item>Quick Start</sl-tree-item>
|
||||
<sl-tree-item>New to Web Components?</sl-tree-item>
|
||||
<sl-tree-item>What Problem Does This Solve?</sl-tree-item>
|
||||
<sl-tree-item>Browser Support</sl-tree-item>
|
||||
<sl-tree-item>License</sl-tree-item>
|
||||
<sl-tree-item>Attribution</sl-tree-item>
|
||||
Maple
|
||||
<sl-tree-item>Field maple</sl-tree-item>
|
||||
<sl-tree-item>Red maple</sl-tree-item>
|
||||
<sl-tree-item>Sugar maple</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item> Installation </sl-tree-item>
|
||||
<sl-tree-item> Usage </sl-tree-item>
|
||||
<sl-tree-item>Oak</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Frameworks
|
||||
<sl-tree-item> React</sl-tree-item>
|
||||
<sl-tree-item> Vue</sl-tree-item>
|
||||
<sl-tree-item> Angular</sl-tree-item>
|
||||
Coniferous
|
||||
<sl-tree-item>Cedar</sl-tree-item>
|
||||
<sl-tree-item>Pine</sl-tree-item>
|
||||
<sl-tree-item>Spruce</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item disabled> Resources </sl-tree-item>
|
||||
<sl-tree-item>
|
||||
Non-trees
|
||||
<sl-tree-item>Bamboo</sl-tree-item>
|
||||
<sl-tree-item>Cactus</sl-tree-item>
|
||||
<sl-tree-item>Fern</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem expanded>
|
||||
Getting Started
|
||||
<SlTreeItem> Overview </SlTreeItem>
|
||||
<SlTreeItem> Installation </SlTreeItem>
|
||||
<SlTreeItem> Usage </SlTreeItem>
|
||||
<SlTreeItem>
|
||||
Deciduous
|
||||
<SlTreeItem>Birch</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
Maple
|
||||
<SlTreeItem>Field maple</SlTreeItem>
|
||||
<SlTreeItem>Red maple</SlTreeItem>
|
||||
<SlTreeItem>Sugar maple</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Oak</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem>
|
||||
Frameworks
|
||||
<SlTreeItem> React</SlTreeItem>
|
||||
<SlTreeItem> Vue</SlTreeItem>
|
||||
<SlTreeItem> Angular</SlTreeItem>
|
||||
Coniferous
|
||||
<SlTreeItem>Cedar</SlTreeItem>
|
||||
<SlTreeItem>Pine</SlTreeItem>
|
||||
<SlTreeItem>Spruce</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem disabled> Resources </SlTreeItem>
|
||||
<SlTreeItem>
|
||||
Non-trees
|
||||
<SlTreeItem>Bamboo</SlTreeItem>
|
||||
<SlTreeItem>Cactus</SlTreeItem>
|
||||
<SlTreeItem>Fern</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
## 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
|
||||
<sl-select id="selection-mode" value="none" label="Selection">
|
||||
<sl-menu-item value="none">none</sl-menu-item>
|
||||
<sl-menu-item value="single">single</sl-menu-item>
|
||||
<sl-menu-item value="leaf">leaf</sl-menu-item>
|
||||
<sl-menu-item value="multiple">multiple</sl-menu-item>
|
||||
<sl-select id="selection-mode" value="single" label="Selection">
|
||||
<sl-menu-item value="single">Single</sl-menu-item>
|
||||
<sl-menu-item value="multiple">Multiple</sl-menu-item>
|
||||
<sl-menu-item value="leaf">Leaf</sl-menu-item>
|
||||
</sl-select>
|
||||
<br />
|
||||
<sl-tree class="selectable">
|
||||
<sl-tree-item expanded>
|
||||
Parent
|
||||
<sl-tree-item expanded>
|
||||
Parent 1
|
||||
<sl-tree-item> Child 1 </sl-tree-item>
|
||||
<sl-tree-item> Child 2 </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-tree class="tree-selectable">
|
||||
<sl-tree-item>
|
||||
Item 1
|
||||
<sl-tree-item>
|
||||
Parent 2
|
||||
<sl-tree-item> Child 1</sl-tree-item>
|
||||
<sl-tree-item> Child 2</sl-tree-item>
|
||||
<sl-tree-item> Child 3</sl-tree-item>
|
||||
Item A
|
||||
<sl-tree-item>Item Z</sl-tree-item>
|
||||
<sl-tree-item>Item Y</sl-tree-item>
|
||||
<sl-tree-item>Item X</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
<style>
|
||||
.selectable sl-tree-item::part(item--selected) {
|
||||
color: var(--sl-color-primary-600);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const selectionMode = document.querySelector('#selection-mode');
|
||||
const treeItem = document.querySelector('.selectable');
|
||||
selectionMode.addEventListener('sl-change', () => {
|
||||
treeItem.selection = selectionMode.value;
|
||||
});
|
||||
})();
|
||||
const selectionMode = document.querySelector('#selection-mode');
|
||||
const tree = document.querySelector('.tree-selectable');
|
||||
|
||||
selectionMode.addEventListener('sl-change', () => {
|
||||
tree.querySelectorAll('sl-tree-item').forEach(item => (item.selected = false));
|
||||
tree.selection = selectionMode.value;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [selection, setSelection] = useState('none');
|
||||
const [selection, setSelection] = useState('single');
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlSelect label="Selection" value={value} onSlChange={event => setSelection(event.target.value)}>
|
||||
<SlMenuItem value="none">none</SlMenuItem>
|
||||
<SlMenuItem value="single">single</SlMenuItem>
|
||||
<SlMenuItem value="leaf">leaf</SlMenuItem>
|
||||
<SlMenuItem value="multiple">multiple</SlMenuItem>
|
||||
<SlMenuItem value="leaf">leaf</SlMenuItem>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
<SlTree selection={selection} class="selectable">
|
||||
<SlTreeItem expanded>
|
||||
Parent
|
||||
<SlTreeItem expanded>
|
||||
Parent 1<SlTreeItem> Child 1 </SlTreeItem>
|
||||
<SlTreeItem> Child 2 </SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTree class="tree-selectable">
|
||||
<SlTreeItem>
|
||||
Item 1
|
||||
<SlTreeItem>
|
||||
Parent 2<SlTreeItem> Child 1</SlTreeItem>
|
||||
<SlTreeItem> Child 2</SlTreeItem>
|
||||
<SlTreeItem> Child 3</SlTreeItem>
|
||||
Item A
|
||||
<SlTreeItem>Item Z</SlTreeItem>
|
||||
<SlTreeItem>Item Y</SlTreeItem>
|
||||
<SlTreeItem>Item X</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 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
|
||||
<sl-tree>
|
||||
<sl-tree-item lazy> Getting Started </sl-tree-item>
|
||||
<sl-tree class="tree-with-lines">
|
||||
<sl-tree-item expanded>
|
||||
Deciduous
|
||||
<sl-tree-item>Birch</sl-tree-item>
|
||||
<sl-tree-item expanded>
|
||||
Maple
|
||||
<sl-tree-item>Field maple</sl-tree-item>
|
||||
<sl-tree-item>Red maple</sl-tree-item>
|
||||
<sl-tree-item>Sugar maple</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Oak</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Coniferous
|
||||
<sl-tree-item>Cedar</sl-tree-item>
|
||||
<sl-tree-item>Pine</sl-tree-item>
|
||||
<sl-tree-item>Spruce</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Non-trees
|
||||
<sl-tree-item>Bamboo</sl-tree-item>
|
||||
<sl-tree-item>Cactus</sl-tree-item>
|
||||
<sl-tree-item>Fern</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree>
|
||||
|
||||
<style>
|
||||
.tree-with-lines {
|
||||
--indent-guide-width: 1px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree class="tree-with-lines" style={{ '--indent-guide-width': '1px' }}>
|
||||
<SlTreeItem expanded>
|
||||
Deciduous
|
||||
<SlTreeItem>Birch</SlTreeItem>
|
||||
<SlTreeItem expanded>
|
||||
Maple
|
||||
<SlTreeItem>Field maple</SlTreeItem>
|
||||
<SlTreeItem>Red maple</SlTreeItem>
|
||||
<SlTreeItem>Sugar maple</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Oak</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem>
|
||||
Coniferous
|
||||
<SlTreeItem>Cedar</SlTreeItem>
|
||||
<SlTreeItem>Pine</SlTreeItem>
|
||||
<SlTreeItem>Spruce</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem>
|
||||
Non-trees
|
||||
<SlTreeItem>Bamboo</SlTreeItem>
|
||||
<SlTreeItem>Cactus</SlTreeItem>
|
||||
<SlTreeItem>Fern</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
### 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
|
||||
<sl-tree selection="leaf">
|
||||
<sl-tree-item lazy>Available Trees</sl-tree-item>
|
||||
</sl-tree>
|
||||
|
||||
<script type="module">
|
||||
const lazyItem = document.querySelector('sl-tree-item[lazy]');
|
||||
lazyItem.addEventListener('sl-lazy-load', () => {
|
||||
// Simulate an asynchronous loading
|
||||
setTimeout(() => {
|
||||
const subItems = ['Overview', 'Installation', 'Usage'];
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
lazyItem.addEventListener('sl-lazy-load', () => {
|
||||
// Simulate asynchronous loading
|
||||
setTimeout(() => {
|
||||
const subItems = ['Birch', 'Cedar', 'Maple', 'Pine'];
|
||||
|
||||
for (const item of subItems) {
|
||||
const treeItem = document.createElement('sl-tree-item');
|
||||
treeItem.innerText = item;
|
||||
|
||||
fragment.appendChild(treeItem);
|
||||
lazyItem.append(treeItem);
|
||||
}
|
||||
lazyItem.appendChild(fragment);
|
||||
|
||||
// Disable lazy mode since the content has been loaded
|
||||
// Disable lazy mode once the content has been loaded
|
||||
lazyItem.lazy = false;
|
||||
}, 2000);
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
@ -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 (
|
||||
<SlTree>
|
||||
<SlTree selection="leaf">
|
||||
<SlTreeItem lazy={lazy} onSlLazyLoad={handleLazyLoad}>
|
||||
Getting Started
|
||||
Available Trees
|
||||
{childItems.map(item => (
|
||||
<SlTreeItem>{item}</SlTreeItem>
|
||||
))}
|
||||
|
@ -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
|
||||
<style>
|
||||
.with-custom-style sl-tree-item::part(item) {
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.with-custom-style sl-tree-item:not([disabled])::part(item):hover,
|
||||
.with-custom-style sl-tree-item:focus-visible::part(item) {
|
||||
color: var(--sl-color-primary-1000);
|
||||
background-color: var(--sl-color-neutral-200);
|
||||
}
|
||||
|
||||
.with-custom-style sl-tree-item::part(item--selected),
|
||||
.with-custom-style sl-tree-item::part(item--selected):hover,
|
||||
.with-custom-style sl-tree-item:focus-visible::part(item--selected) {
|
||||
color: var(--sl-color-neutral-1000);
|
||||
background-color: var(--sl-color-neutral-100);
|
||||
border-left-color: var(--sl-color-primary-600);
|
||||
}
|
||||
</style>
|
||||
<sl-tree selection="leaf" class="with-custom-style">
|
||||
<sl-tree class="tree-with-icons">
|
||||
<sl-tree-item expanded>
|
||||
Getting Started
|
||||
<sl-icon name="folder"></sl-icon>
|
||||
Root
|
||||
|
||||
<sl-tree-item>
|
||||
Overview
|
||||
<sl-tree-item>Quick Start</sl-tree-item>
|
||||
<sl-tree-item>New to Web Components?</sl-tree-item>
|
||||
<sl-tree-item>What Problem Does This Solve?</sl-tree-item>
|
||||
<sl-tree-item>Browser Support</sl-tree-item>
|
||||
<sl-tree-item>License</sl-tree-item>
|
||||
<sl-tree-item>Attribution</sl-tree-item>
|
||||
<sl-icon name="folder"> </sl-icon>
|
||||
Folder 1
|
||||
<sl-tree-item>
|
||||
<sl-icon name="files"></sl-icon>
|
||||
File 1 - 1
|
||||
</sl-tree-item>
|
||||
<sl-tree-item disabled>
|
||||
<sl-icon name="files"></sl-icon>
|
||||
File 1 - 2
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="files"></sl-icon>
|
||||
File 1 - 3
|
||||
</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item selected> Installation </sl-tree-item>
|
||||
<sl-tree-item> Usage </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Frameworks
|
||||
<sl-tree-item> React</sl-tree-item>
|
||||
<sl-tree-item> Vue</sl-tree-item>
|
||||
<sl-tree-item> Angular</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item disabled> Resources </sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
### With indentation lines
|
||||
|
||||
```html preview
|
||||
<style>
|
||||
.with-indentation-lines sl-tree-item[expanded]::part(children) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.with-indentation-lines sl-tree-item[expanded]::part(children)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 1em;
|
||||
top: var(--sl-spacing-2x-small);
|
||||
bottom: var(--sl-spacing-2x-small);
|
||||
border-right: 1px solid var(--sl-color-neutral-100);
|
||||
transition: 0.2s border-right ease-in-out;
|
||||
}
|
||||
|
||||
.with-indentation-lines sl-tree-item[expanded]::part(children):hover::before {
|
||||
border-right: 1px solid var(--sl-color-neutral-600);
|
||||
}
|
||||
</style>
|
||||
<sl-tree class="with-indentation-lines">
|
||||
<sl-tree-item expanded>
|
||||
Getting Started
|
||||
<sl-tree-item>
|
||||
Overview
|
||||
<sl-tree-item>Quick Start</sl-tree-item>
|
||||
<sl-tree-item>New to Web Components?</sl-tree-item>
|
||||
<sl-tree-item>What Problem Does This Solve?</sl-tree-item>
|
||||
<sl-tree-item>Browser Support</sl-tree-item>
|
||||
<sl-tree-item>License</sl-tree-item>
|
||||
<sl-tree-item>Attribution</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item> Installation </sl-tree-item>
|
||||
<sl-tree-item> Usage </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Frameworks
|
||||
<sl-tree-item> React</sl-tree-item>
|
||||
<sl-tree-item> Vue</sl-tree-item>
|
||||
<sl-tree-item> Angular</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item disabled> Resources </sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
### With icons
|
||||
|
||||
```html preview
|
||||
<style>
|
||||
sl-icon {
|
||||
margin-right: var(--sl-spacing-x-small);
|
||||
}
|
||||
</style>
|
||||
<sl-tree>
|
||||
<sl-tree-item expanded>
|
||||
<sl-icon name="folder"></sl-icon>Root
|
||||
<sl-tree-item>
|
||||
<sl-icon name="folder"> </sl-icon>Folder 1
|
||||
<sl-tree-item> <sl-icon name="files"></sl-icon>File 1 - 1 </sl-tree-item>
|
||||
<sl-tree-item disabled> <sl-icon name="files"></sl-icon>File 1 - 2 </sl-tree-item>
|
||||
<sl-tree-item> <sl-icon name="files"></sl-icon>File 1 - 3 </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="files"></sl-icon>
|
||||
Folder 2
|
||||
<sl-tree-item selected> <sl-icon name="files"></sl-icon>File 2 - 1 </sl-tree-item>
|
||||
<sl-tree-item> <sl-icon name="files"></sl-icon>File 2 - 2 </sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="files"></sl-icon>
|
||||
File 2 - 1
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="files"></sl-icon>
|
||||
File 2 - 2
|
||||
</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="files"></sl-icon>
|
||||
File 1
|
||||
</sl-tree-item>
|
||||
<sl-tree-item> <sl-icon name="files"></sl-icon>File 1 </sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
```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 (
|
||||
<SlTree class="tree-with-icons">
|
||||
<SlTreeItem expanded>
|
||||
<SlIcon name="folder" />
|
||||
Root
|
||||
<SlTreeItem>
|
||||
<SlIcon name="folder" />
|
||||
Folder 1<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 1 - 1
|
||||
</SlTreeItem>
|
||||
<SlTreeItem disabled>
|
||||
<SlIcon name="files" />
|
||||
File 1 - 2
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 1 - 3
|
||||
</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
Folder 2<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 2 - 1
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 2 - 2
|
||||
</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 1
|
||||
</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-tree]
|
||||
|
|
|
@ -10,6 +10,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
|||
|
||||
## Next
|
||||
|
||||
- Added experimental `<sl-tree>` and `<sl-tree-item>` components [#823](https://github.com/shoelace-style/shoelace/pull/823)
|
||||
- Added `--indicator-width` custom property to `<sl-progress-ring>` [#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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('<sl-tree-item>', () => {
|
|||
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('<sl-tree-item>', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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('<sl-tree-item>', () => {
|
|||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`
|
||||
<div part="base" class="tree-item">
|
||||
<div
|
||||
part="base"
|
||||
class="${classMap({
|
||||
'tree-item': true,
|
||||
'tree-item--selected': this.selected,
|
||||
'tree-item--disabled': this.disabled,
|
||||
'tree-item--leaf': this.isLeaf,
|
||||
'tree-item--rtl': this.localize.dir() === 'rtl'
|
||||
})}"
|
||||
>
|
||||
<div
|
||||
class="${classMap({
|
||||
'tree-item__item': true,
|
||||
'tree-item__item--selected': this.selected,
|
||||
'tree-item__item--disabled': this.disabled
|
||||
})}"
|
||||
part="${stringMap({
|
||||
item: true,
|
||||
'item--selected': this.selected,
|
||||
'item--disabled': this.disabled
|
||||
})}"
|
||||
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>
|
||||
|
||||
<div class="tree-item__expand-button" aria-hidden="true" @click="${this.handleToggleExpand}">
|
||||
<div
|
||||
class=${classMap({
|
||||
'tree-item__expand-button': true,
|
||||
'tree-item__expand-button--visible': !this.loading && (!this.isLeaf || this.lazy)
|
||||
})}
|
||||
aria-hidden="true"
|
||||
@click="${this.handleToggleExpand}"
|
||||
>
|
||||
${when(this.loading, () => html` <sl-spinner></sl-spinner> `)}
|
||||
${when(
|
||||
!this.loading && (!this.isLeaf || this.lazy),
|
||||
() => html`
|
||||
<sl-icon library="system" name="${this.expanded ? 'chevron-down' : 'chevron-right'}"></sl-icon>
|
||||
<sl-icon
|
||||
library="system"
|
||||
name="${this.expanded ? 'chevron-down' : isRtl ? 'chevron-left' : 'chevron-right'}"
|
||||
></sl-icon>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -5,10 +5,22 @@ export default css`
|
|||
${componentStyles}
|
||||
|
||||
:host {
|
||||
/*
|
||||
* These are actually used by tree item, but we define them here so they can more easily be set and all tree items
|
||||
* stay consistent.
|
||||
*/
|
||||
--indent-guide-color: var(--sl-color-neutral-200);
|
||||
--indent-guide-offset: 0;
|
||||
--indent-guide-style: solid;
|
||||
--indent-guide-width: 0;
|
||||
--indent-size: var(--sl-spacing-large);
|
||||
|
||||
display: block;
|
||||
/**
|
||||
* tree-item indentation uses the "em" unit in order to increment its width on each level,
|
||||
* so setting the font size to zero here, removes the indentation for all the nodes in the first level.
|
||||
isolation: isolate;
|
||||
|
||||
/*
|
||||
* Tree item indentation uses the "em" unit to increment its width on each level, so setting the font size to zero
|
||||
* here removes the indentation for all the nodes on the first level.
|
||||
*/
|
||||
font-size: 0;
|
||||
}
|
||||
|
|
|
@ -207,78 +207,6 @@ describe('<sl-tree>', () => {
|
|||
});
|
||||
|
||||
describe('when Enter is pressed', () => {
|
||||
describe('and selection is "none"', () => {
|
||||
describe('and node is expanded', () => {
|
||||
it('should not select the tree item', async () => {
|
||||
// Arrange
|
||||
const parentNode = el.children[2] as SlTreeItem;
|
||||
parentNode.focus();
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
await sendKeys({ press: 'Enter' });
|
||||
|
||||
// Assert
|
||||
expect(el).to.have.attribute('tabindex', '-1');
|
||||
expect(parentNode).to.have.attribute('tabindex', '0');
|
||||
expect(parentNode).to.have.attribute('expanded');
|
||||
expect(parentNode).not.to.have.attribute('selected');
|
||||
});
|
||||
|
||||
it('should collapse the tree item', async () => {
|
||||
// Arrange
|
||||
const parentNode = el.children[2] as SlTreeItem;
|
||||
parentNode.expanded = true;
|
||||
parentNode.focus();
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
await sendKeys({ press: 'Enter' });
|
||||
|
||||
// Assert
|
||||
expect(el).to.have.attribute('tabindex', '-1');
|
||||
expect(parentNode).to.have.attribute('tabindex', '0');
|
||||
expect(parentNode).not.to.have.attribute('expanded');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and node is collapsed', () => {
|
||||
describe('and selection is "none"', () => {
|
||||
it('should not select the tree item', async () => {
|
||||
// Arrange
|
||||
const parentNode = el.children[2] as SlTreeItem;
|
||||
parentNode.focus();
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
await sendKeys({ press: 'Enter' });
|
||||
|
||||
// Assert
|
||||
expect(el).to.have.attribute('tabindex', '-1');
|
||||
expect(parentNode).to.have.attribute('tabindex', '0');
|
||||
expect(parentNode).to.have.attribute('expanded');
|
||||
expect(parentNode).not.to.have.attribute('selected');
|
||||
});
|
||||
|
||||
it('should expand the tree item', async () => {
|
||||
// Arrange
|
||||
const parentNode = el.children[2] as SlTreeItem;
|
||||
parentNode.focus();
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
await sendKeys({ press: 'Enter' });
|
||||
|
||||
// Assert
|
||||
expect(el).to.have.attribute('tabindex', '-1');
|
||||
expect(parentNode).to.have.attribute('tabindex', '0');
|
||||
expect(parentNode).to.have.attribute('expanded');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and selection is "single"', () => {
|
||||
it('should select only one tree item', async () => {
|
||||
// Arrange
|
||||
|
@ -353,25 +281,6 @@ describe('<sl-tree>', () => {
|
|||
});
|
||||
|
||||
describe('when Space is pressed', () => {
|
||||
describe('and selection is "none"', () => {
|
||||
it('should not select the tree item', async () => {
|
||||
// Arrange
|
||||
el.selection = 'none';
|
||||
const node = el.children[0] as SlTreeItem;
|
||||
node.focus();
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
await sendKeys({ press: ' ' });
|
||||
|
||||
// Assert
|
||||
expect(el).to.have.attribute('tabindex', '-1');
|
||||
expect(node).to.have.attribute('tabindex', '0');
|
||||
expect(node).not.to.have.attribute('expanded');
|
||||
expect(node).not.to.have.attribute('selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and selection is "single"', () => {
|
||||
it('should select only one tree item', async () => {
|
||||
// Arrange
|
||||
|
@ -484,26 +393,6 @@ describe('<sl-tree>', () => {
|
|||
});
|
||||
|
||||
describe('when the user clicks on a tree item', () => {
|
||||
describe('and selection is "none"', () => {
|
||||
it('should not select the tree item', async () => {
|
||||
// Arrange
|
||||
const node = el.children[1] as SlTreeItem;
|
||||
const selectedChangeSpy = sinon.spy();
|
||||
el.addEventListener('sl-selected-change', selectedChangeSpy);
|
||||
|
||||
// Act
|
||||
node.focus();
|
||||
node.click();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(el).to.have.attribute('tabindex', '-1');
|
||||
expect(node).to.have.attribute('tabindex', '0');
|
||||
expect(node).not.to.have.attribute('selected');
|
||||
expect(selectedChangeSpy).not.to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('and selection is "single"', () => {
|
||||
it('should select only one tree item', async () => {
|
||||
// Arrange
|
||||
|
@ -622,13 +511,13 @@ describe('<sl-tree>', () => {
|
|||
});
|
||||
|
||||
describe('when an tree item gets selected or deselected', () => {
|
||||
it('should emit a `sl-selected-change` event', async () => {
|
||||
it('should emit a `sl-selection-change` event', async () => {
|
||||
// Arrange
|
||||
el.selection = 'single';
|
||||
await el.updateComplete;
|
||||
|
||||
const selectedChangeSpy = sinon.spy();
|
||||
el.addEventListener('sl-selected-change', selectedChangeSpy);
|
||||
el.addEventListener('sl-selection-change', selectedChangeSpy);
|
||||
|
||||
const node = el.children[0] as SlTreeItem;
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { customElement, property, query } from 'lit/decorators.js';
|
|||
import { emit } from 'src/internal/event';
|
||||
import { clamp } from 'src/internal/math';
|
||||
import { watch } from 'src/internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { isTreeItem } from '../tree-item/tree-item';
|
||||
import styles from './tree.styles';
|
||||
import type SlTreeItem from '../tree-item/tree-item';
|
||||
|
@ -38,12 +39,17 @@ function syncCheckboxes(changedTreeItem: SlTreeItem) {
|
|||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
* @event sl-selected-change - Emitted when an item gets selected or deselected
|
||||
* @event {{ selection: this.selectedItems }} sl-selection-change - Emitted when an item gets selected or deselected
|
||||
*
|
||||
* @slot - The default slot.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
*
|
||||
* @cssproperty [--indent-size=var(--sl-spacing-medium)] - The size of the indentation for nested items.
|
||||
* @cssproperty [--indent-guide-color=var(--sl-color-neutral-200)] - The color of the indentation line.
|
||||
* @cssproperty [--indent-guide-offset=0] - The amount of vertical spacing to leave between the top and bottom of the indentation line's starting position.
|
||||
* @cssproperty [--indent-guide-style=solid] - The style of the indentation line, e.g. solid, dotted, dashed.
|
||||
* @cssproperty [--indent-guide-width=0] - The width of the indentation line.
|
||||
*/
|
||||
@customElement('sl-tree')
|
||||
export default class SlTree extends LitElement {
|
||||
|
@ -52,14 +58,15 @@ export default class SlTree extends LitElement {
|
|||
@query('slot') defaultSlot: HTMLSlotElement;
|
||||
|
||||
/** Specifies the selection behavior of the Tree */
|
||||
@property() selection: 'none' | 'single' | 'multiple' | 'leaf' = 'none';
|
||||
@property() selection: 'single' | 'multiple' | 'leaf' = 'single';
|
||||
|
||||
/**
|
||||
* @internal A collection of all the items in the tree, in the order they appear.
|
||||
* The collection is live, it means that it is automatically updated when the underlying document is changed.
|
||||
*/
|
||||
//
|
||||
// A collection of all the items in the tree, in the order they appear. The collection is live, meaning it is
|
||||
// automatically updated when the underlying document is changed.
|
||||
//
|
||||
private treeItems: HTMLCollectionOf<SlTreeItem> = this.getElementsByTagName('sl-tree-item');
|
||||
private lastFocusedItem: SlTreeItem;
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private mutationObserver: MutationObserver;
|
||||
|
||||
connectedCallback(): void {
|
||||
|
@ -125,8 +132,6 @@ export default class SlTree extends LitElement {
|
|||
}
|
||||
|
||||
selectItem(selectedItem: SlTreeItem) {
|
||||
if (this.selection === 'none') return;
|
||||
|
||||
if (this.selection === 'multiple') {
|
||||
selectedItem.selected = !selectedItem.selected;
|
||||
if (selectedItem.lazy) {
|
||||
|
@ -141,12 +146,10 @@ export default class SlTree extends LitElement {
|
|||
selectedItem.expanded = !selectedItem.expanded;
|
||||
}
|
||||
|
||||
emit(this, 'sl-selected-change', { detail: this.selectedItems });
|
||||
emit(this, 'sl-selection-change', { detail: { selection: this.selectedItems } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of tree items that are selected in the tree
|
||||
*/
|
||||
// Returns the list of tree items that are selected in the tree.
|
||||
get selectedItems(): SlTreeItem[] {
|
||||
const items = [...this.treeItems];
|
||||
const isSelected = (item: SlTreeItem) => item.selected;
|
||||
|
@ -171,8 +174,13 @@ export default class SlTree extends LitElement {
|
|||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
if (!['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft', 'Home', 'End', 'Enter', ' '].includes(event.key)) return;
|
||||
if (!['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft', 'Home', 'End', 'Enter', ' '].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = this.getFocusableItems();
|
||||
const isLtr = this.localize.dir() === 'ltr';
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
|
||||
if (items.length > 0) {
|
||||
event.preventDefault();
|
||||
|
@ -188,69 +196,48 @@ export default class SlTree extends LitElement {
|
|||
};
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
/**
|
||||
* Moves focus to the next node that is focusable without opening or closing a node.
|
||||
*/
|
||||
// Moves focus to the next node that is focusable without opening or closing a node.
|
||||
focusItemAt(activeItemIndex + 1);
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
/**
|
||||
* Moves focus to the next node that is focusable without opening or closing a node.
|
||||
*/
|
||||
// Moves focus to the next node that is focusable without opening or closing a node.
|
||||
focusItemAt(activeItemIndex - 1);
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
/**
|
||||
* When focus is on a closed node, opens the node; focus does not move.
|
||||
* When focus is on a open node, moves focus to the first child node.
|
||||
* When focus is on an end node (a tree item with no children), does nothing.
|
||||
*/
|
||||
} else if ((isLtr && event.key === 'ArrowRight') || (isRtl && event.key === 'ArrowLeft')) {
|
||||
//
|
||||
// When focus is on a closed node, opens the node; focus does not move.
|
||||
// When focus is on a open node, moves focus to the first child node.
|
||||
// When focus is on an end node (a tree item with no children), does nothing.
|
||||
//
|
||||
if (!activeItem || activeItem.expanded || (activeItem.isLeaf && !activeItem.lazy)) {
|
||||
focusItemAt(activeItemIndex + 1);
|
||||
} else {
|
||||
toggleExpand(true);
|
||||
}
|
||||
} else if (event.key === 'ArrowLeft') {
|
||||
/**
|
||||
* When focus is on an open node, closes the node.
|
||||
* When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
|
||||
* When focus is on a closed `tree`, does nothing.
|
||||
*/
|
||||
} else if ((isLtr && event.key === 'ArrowLeft') || (isRtl && event.key === 'ArrowRight')) {
|
||||
//
|
||||
// When focus is on an open node, closes the node.
|
||||
// When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
|
||||
// When focus is on a closed `tree`, does nothing.
|
||||
//
|
||||
if (!activeItem || activeItem.isLeaf || !activeItem.expanded) {
|
||||
focusItemAt(activeItemIndex - 1);
|
||||
} else {
|
||||
toggleExpand(false);
|
||||
}
|
||||
} else if (event.key === 'Home') {
|
||||
/**
|
||||
* Moves focus to the first node in the tree without opening or closing a node.
|
||||
*/
|
||||
// Moves focus to the first node in the tree without opening or closing a node.
|
||||
focusItemAt(0);
|
||||
} else if (event.key === 'End') {
|
||||
/**
|
||||
* Moves focus to the last node in the tree that is focusable without opening the node.
|
||||
*/
|
||||
// Moves focus to the last node in the tree that is focusable without opening the node.
|
||||
focusItemAt(items.length - 1);
|
||||
} else if (event.key === 'Enter') {
|
||||
/**
|
||||
* Performs the default action of the currently focused node. For parent nodes, it opens or closes the node.
|
||||
* In single-select trees, if the node has no children, selects the current node if not already selected (which
|
||||
* is the default action).
|
||||
*/
|
||||
if (['none', 'leaf'].includes(this.selection) && !activeItem.isLeaf) {
|
||||
toggleExpand(!activeItem.expanded);
|
||||
} else {
|
||||
this.selectItem(activeItem);
|
||||
}
|
||||
} else if (event.key === ' ') {
|
||||
/**
|
||||
* Toggles the selection state of the focused node.
|
||||
*/
|
||||
} else if (event.key === 'Enter' || event.key === ' ') {
|
||||
// Selects the focused node.
|
||||
this.selectItem(activeItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(e: Event) {
|
||||
const target = e.target as HTMLElement;
|
||||
handleClick(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const treeItem = target.closest('sl-tree-item')!;
|
||||
|
||||
if (!treeItem.disabled) {
|
||||
|
@ -258,8 +245,8 @@ export default class SlTree extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
handleFocusOut = (e: FocusEvent) => {
|
||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||
handleFocusOut = (event: FocusEvent) => {
|
||||
const relatedTarget = event.relatedTarget as HTMLElement;
|
||||
|
||||
// If the element that got the focus is not in the tree
|
||||
if (!relatedTarget || !this.contains(relatedTarget)) {
|
||||
|
@ -267,11 +254,11 @@ export default class SlTree extends LitElement {
|
|||
}
|
||||
};
|
||||
|
||||
handleFocusIn = (e: FocusEvent) => {
|
||||
const target = e.target as SlTreeItem;
|
||||
handleFocusIn = (event: FocusEvent) => {
|
||||
const target = event.target as SlTreeItem;
|
||||
|
||||
// If the tree has been focused, move the focus to the last focused item
|
||||
if (e.target === this) {
|
||||
if (event.target === this) {
|
||||
this.focusItem(this.lastFocusedItem || this.treeItems[0]);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,3 @@
|
|||
export function uppercaseFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
export function stringMap(...parts: (string | Record<string, boolean>)[]): string | undefined {
|
||||
return parts
|
||||
.map(part => (typeof part === 'object' ? Object.keys(part).filter((key: string) => part[key]) : part))
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
|
|
@ -50,9 +50,9 @@ export { default as SlTabPanel } from './components/tab-panel/tab-panel';
|
|||
export { default as SlTag } from './components/tag/tag';
|
||||
export { default as SlTextarea } from './components/textarea/textarea';
|
||||
export { default as SlTooltip } from './components/tooltip/tooltip';
|
||||
export { default as SlVisuallyHidden } from './components/visually-hidden/visually-hidden';
|
||||
export { default as SlTree } from './components/tree/tree';
|
||||
export { default as SlTreeItem } from './components/tree-item/tree-item';
|
||||
export { default as SlVisuallyHidden } from './components/visually-hidden/visually-hidden';
|
||||
/* plop:component */
|
||||
|
||||
// Utilities
|
||||
|
|
Ładowanie…
Reference in New Issue