@sproutsocial/seeds-react-menu 1.2.3 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +16 -0
- package/dist/esm/index.js +156 -113
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +168 -126
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Autocomplete/MultiAutocomplete.tsx +3 -0
- package/src/Autocomplete/SingleAutocomplete.tsx +3 -7
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +21 -0
- package/src/MenuContent/DefaultMenuEmptyState.tsx +17 -0
- package/src/MenuContent/MenuContent.tsx +15 -2
- package/src/MenuContent/MenuContentTypes.ts +2 -0
- package/src/MenuContent/__tests__/MenuContent.test.tsx +38 -17
- package/src/MenuContext.tsx +14 -0
- package/src/MenuItem/__tests__/MenuItem.test.tsx +11 -14
- package/src/MenuRoot/MenuRoot.tsx +9 -2
- package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +44 -0
- package/src/menuTestingUtils.tsx +12 -7
package/package.json
CHANGED
|
@@ -162,6 +162,7 @@ export const MultiAutocomplete = <
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
const { isOpen, closeMenu, inputValue } = useComboboxReturnProps;
|
|
165
|
+
const { popoutProps: userPopoutProps } = useComboboxProps;
|
|
165
166
|
|
|
166
167
|
React.useEffect(() => {
|
|
167
168
|
if (!inputValue) return;
|
|
@@ -189,7 +190,9 @@ export const MultiAutocomplete = <
|
|
|
189
190
|
}}
|
|
190
191
|
popoutProps={{
|
|
191
192
|
focusOnContent: false,
|
|
193
|
+
...userPopoutProps,
|
|
192
194
|
}}
|
|
195
|
+
autoCloseOnEmpty
|
|
193
196
|
>
|
|
194
197
|
{children}
|
|
195
198
|
</MenuRootComponent>
|
|
@@ -57,13 +57,7 @@ export const SingleAutocomplete = <
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
const { isOpen, closeMenu, inputValue } = useComboboxReturnProps;
|
|
60
|
-
|
|
61
|
-
React.useEffect(() => {
|
|
62
|
-
if (!inputValue) return;
|
|
63
|
-
if (allMenuItems && hiddenItemsCount >= allMenuItems.length && isOpen) {
|
|
64
|
-
closeMenu?.();
|
|
65
|
-
}
|
|
66
|
-
}, [hiddenItemsCount, allMenuItems?.length, isOpen, inputValue]);
|
|
60
|
+
const { popoutProps: userPopoutProps } = useComboboxProps;
|
|
67
61
|
|
|
68
62
|
return (
|
|
69
63
|
<MenuRootComponent
|
|
@@ -77,7 +71,9 @@ export const SingleAutocomplete = <
|
|
|
77
71
|
}}
|
|
78
72
|
popoutProps={{
|
|
79
73
|
focusOnContent: false,
|
|
74
|
+
...userPopoutProps,
|
|
80
75
|
}}
|
|
76
|
+
autoCloseOnEmpty
|
|
81
77
|
>
|
|
82
78
|
{children}
|
|
83
79
|
</MenuRootComponent>
|
|
@@ -189,6 +189,27 @@ describe("Autocomplete Component", () => {
|
|
|
189
189
|
});
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
it("should apply custom popoutProps.width to the popout container", async () => {
|
|
193
|
+
const { user } = render(
|
|
194
|
+
<Autocomplete
|
|
195
|
+
{...defaultAutocompleteProps}
|
|
196
|
+
popoutProps={{ width: 300 }}
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const inputElement = screen.getByRole("combobox");
|
|
201
|
+
|
|
202
|
+
await act(async () => {
|
|
203
|
+
await user.click(inputElement);
|
|
204
|
+
await user.type(inputElement, "item");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const listbox = screen.getByRole("listbox");
|
|
208
|
+
const popoutContainer = listbox.parentElement;
|
|
209
|
+
|
|
210
|
+
expect(popoutContainer).toHaveStyle({ width: "300px" });
|
|
211
|
+
});
|
|
212
|
+
|
|
192
213
|
describe("in multiSelect mode", () => {
|
|
193
214
|
it("should render multiple selected items", () => {
|
|
194
215
|
render(
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
|
|
3
|
+
const StyledEmptyState = styled.div`
|
|
4
|
+
padding: ${({ theme }) => theme.space[350]};
|
|
5
|
+
text-align: center;
|
|
6
|
+
font-size: ${({ theme }) => theme.typography[200].fontSize};
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
const DefaultMenuEmptyState = ({
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
}) => {
|
|
14
|
+
return <StyledEmptyState>{children || "No results found."}</StyledEmptyState>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { DefaultMenuEmptyState };
|
|
@@ -4,6 +4,7 @@ import { useMenuContentContext } from "../MenuContext";
|
|
|
4
4
|
import { mapMenuItems } from "../hooks/useMenuChildren";
|
|
5
5
|
import { useCallback } from "react";
|
|
6
6
|
import type { TypeMenuItemProps } from "../MenuItem";
|
|
7
|
+
import { DefaultMenuEmptyState } from "./DefaultMenuEmptyState";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @link https://seeds.sproutsocial.com/components/seeds-react-menu/#menucontent
|
|
@@ -35,10 +36,17 @@ const MenuContent = <I extends TypeMenuItemProps = TypeMenuItemProps>({
|
|
|
35
36
|
MenuItemComponent,
|
|
36
37
|
onKeyDown,
|
|
37
38
|
innerRef = null,
|
|
39
|
+
emptyStateElement,
|
|
38
40
|
...rest
|
|
39
41
|
}: TypeMenuContentProps<I>) => {
|
|
40
|
-
const {
|
|
41
|
-
|
|
42
|
+
const {
|
|
43
|
+
getToggleButtonProps,
|
|
44
|
+
getMenuProps,
|
|
45
|
+
isListbox,
|
|
46
|
+
hasVisibleItems,
|
|
47
|
+
autoCloseOnEmpty,
|
|
48
|
+
isInitialized,
|
|
49
|
+
} = useMenuContentContext();
|
|
42
50
|
const toggleButtonProps = getToggleButtonProps?.();
|
|
43
51
|
|
|
44
52
|
const children =
|
|
@@ -46,6 +54,11 @@ const MenuContent = <I extends TypeMenuItemProps = TypeMenuItemProps>({
|
|
|
46
54
|
? childrenProp
|
|
47
55
|
: mapMenuItems({ menuItems, MenuItemComponent });
|
|
48
56
|
|
|
57
|
+
if (isInitialized && !hasVisibleItems && !autoCloseOnEmpty) {
|
|
58
|
+
return emptyStateElement ?? <DefaultMenuEmptyState />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Hides the content if there are no children and it shouldn't show the empty state
|
|
49
62
|
if (!children) {
|
|
50
63
|
return null;
|
|
51
64
|
}
|
|
@@ -5,4 +5,6 @@ export type TypeMenuContentProps<
|
|
|
5
5
|
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
6
6
|
> = Omit<React.ComponentPropsWithoutRef<"ul">, "children"> & {
|
|
7
7
|
innerRef?: React.Ref<HTMLUListElement>;
|
|
8
|
+
/* Custom component to display when no menu items are visible (e.g. "No results found"). */
|
|
9
|
+
emptyStateElement?: React.ReactNode;
|
|
8
10
|
} & TypeChildrenOrItems<I>;
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
2
2
|
import { MenuFooter, MenuHeader, MenuContent } from "../../index";
|
|
3
3
|
import Button from "@sproutsocial/seeds-react-button";
|
|
4
|
+
import {
|
|
5
|
+
testMenuToggleElement,
|
|
6
|
+
TestWithMenuRoot,
|
|
7
|
+
} from "../../menuTestingUtils";
|
|
8
|
+
|
|
9
|
+
const mockMenuContext = {
|
|
10
|
+
isOpen: true,
|
|
11
|
+
};
|
|
4
12
|
|
|
5
13
|
describe("MenuContent API", () => {
|
|
6
14
|
it("should render correctly with default props", () => {
|
|
7
15
|
render(
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
<TestWithMenuRoot
|
|
17
|
+
menuToggleElement={testMenuToggleElement}
|
|
18
|
+
{...mockMenuContext}
|
|
19
|
+
>
|
|
20
|
+
<MenuContent data-testid="menu-content">
|
|
21
|
+
<div>Test Child</div>
|
|
22
|
+
</MenuContent>
|
|
23
|
+
</TestWithMenuRoot>
|
|
11
24
|
);
|
|
25
|
+
|
|
12
26
|
const menuContent = screen.getByTestId("menu-content");
|
|
13
27
|
expect(menuContent).toBeInTheDocument();
|
|
14
28
|
expect(menuContent).toHaveAttribute("tabIndex", "0");
|
|
@@ -18,21 +32,28 @@ describe("MenuContent API", () => {
|
|
|
18
32
|
describe("MenuContent Behavior", () => {
|
|
19
33
|
it("focus should move properly through menu", async () => {
|
|
20
34
|
const { user } = render(
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
<TestWithMenuRoot
|
|
36
|
+
menuToggleElement={testMenuToggleElement}
|
|
37
|
+
{...mockMenuContext}
|
|
38
|
+
>
|
|
39
|
+
<MenuContent>
|
|
40
|
+
<div>
|
|
41
|
+
<MenuHeader>
|
|
42
|
+
<Button data-testid="menu-header-button" onClick={() => {}}>
|
|
43
|
+
Button
|
|
44
|
+
</Button>
|
|
45
|
+
</MenuHeader>
|
|
46
|
+
<MenuContent data-testid="menu-content">
|
|
47
|
+
<div>Test Child</div>
|
|
48
|
+
</MenuContent>
|
|
49
|
+
<MenuFooter>
|
|
50
|
+
<Button data-testid="menu-footer-button" onClick={() => {}}>
|
|
51
|
+
Button 2
|
|
52
|
+
</Button>
|
|
53
|
+
</MenuFooter>
|
|
54
|
+
</div>
|
|
29
55
|
</MenuContent>
|
|
30
|
-
|
|
31
|
-
<Button data-testid="menu-footer-button" onClick={() => {}}>
|
|
32
|
-
Button 2
|
|
33
|
-
</Button>
|
|
34
|
-
</MenuFooter>
|
|
35
|
-
</div>
|
|
56
|
+
</TestWithMenuRoot>
|
|
36
57
|
);
|
|
37
58
|
|
|
38
59
|
const menuHeaderButton = screen.getByTestId("menu-header-button");
|
package/src/MenuContext.tsx
CHANGED
|
@@ -111,6 +111,8 @@ export interface TypeMenuContextProps
|
|
|
111
111
|
itemsMap?: { [key: string]: TypeInternalItemProps };
|
|
112
112
|
selectedItemIds?: { [key: string]: boolean };
|
|
113
113
|
isListbox?: boolean;
|
|
114
|
+
hasVisibleItems?: boolean;
|
|
115
|
+
autoCloseOnEmpty?: boolean;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
export interface TypeMenuContext extends TypeMenuContextProps {
|
|
@@ -120,6 +122,7 @@ export interface TypeMenuContext extends TypeMenuContextProps {
|
|
|
120
122
|
props: Pick<TypeTokenInputProps, "inputProps"> &
|
|
121
123
|
Parameters<Required<TypeMenuContextProps>["getInputProps"]>[0]
|
|
122
124
|
) => Omit<TypeDownshiftMergedInputProps<TypeTokenInputProps>, "onClick">;
|
|
125
|
+
isInitialized?: boolean;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
export interface TypeMenuProviderProps {
|
|
@@ -149,6 +152,8 @@ export const defaultMenuContext: TypeMenuContext = {
|
|
|
149
152
|
itemsMap: {},
|
|
150
153
|
isItemSelected: () => false,
|
|
151
154
|
getInputComponentProps: () => ({}),
|
|
155
|
+
hasVisibleItems: false,
|
|
156
|
+
isInitialized: false,
|
|
152
157
|
};
|
|
153
158
|
|
|
154
159
|
export const getSelectedItemIds = <
|
|
@@ -185,6 +190,8 @@ export const useMenu = ({
|
|
|
185
190
|
selectedItems,
|
|
186
191
|
getInputProps,
|
|
187
192
|
contentRef: contentRefFromProps,
|
|
193
|
+
autoCloseOnEmpty,
|
|
194
|
+
hiddenItemsCount,
|
|
188
195
|
...rest
|
|
189
196
|
}: Partial<TypeMenuContextProps> = {}): TypeMenuContext => {
|
|
190
197
|
const contentRef = contentRefFromProps || React.createRef<HTMLUListElement>();
|
|
@@ -247,6 +254,10 @@ export const useMenu = ({
|
|
|
247
254
|
[getInputProps]
|
|
248
255
|
);
|
|
249
256
|
|
|
257
|
+
const hasVisibleItems = useMemo(() => {
|
|
258
|
+
return (hiddenItemsCount ?? 0) < items.length;
|
|
259
|
+
}, [items, hiddenItemsCount]);
|
|
260
|
+
|
|
250
261
|
return {
|
|
251
262
|
isOpen,
|
|
252
263
|
isListbox,
|
|
@@ -259,6 +270,9 @@ export const useMenu = ({
|
|
|
259
270
|
getInputProps,
|
|
260
271
|
getInputComponentProps,
|
|
261
272
|
contentRef,
|
|
273
|
+
hasVisibleItems,
|
|
274
|
+
autoCloseOnEmpty,
|
|
275
|
+
isInitialized: true,
|
|
262
276
|
...menuProps,
|
|
263
277
|
};
|
|
264
278
|
};
|
|
@@ -18,7 +18,11 @@ import {
|
|
|
18
18
|
MultiSelectMenu,
|
|
19
19
|
INPUT_TYPES,
|
|
20
20
|
} from "../../index";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
testMenuToggleElement,
|
|
23
|
+
TestWithMenuRoot,
|
|
24
|
+
TestWithMockedMenu,
|
|
25
|
+
} from "../../menuTestingUtils";
|
|
22
26
|
|
|
23
27
|
const books = [
|
|
24
28
|
{
|
|
@@ -35,19 +39,6 @@ const menuItemProps = {
|
|
|
35
39
|
id: "book-id",
|
|
36
40
|
children: "Book Title",
|
|
37
41
|
};
|
|
38
|
-
const testMenuContext = {
|
|
39
|
-
isOpen: true,
|
|
40
|
-
closeMenu: jest.fn(),
|
|
41
|
-
openMenu: jest.fn(),
|
|
42
|
-
getMenuProps: jest.fn((props) => props),
|
|
43
|
-
getToggleButtonProps: jest.fn((props) => ({
|
|
44
|
-
onKeyDown: jest.fn(),
|
|
45
|
-
...props,
|
|
46
|
-
})),
|
|
47
|
-
getItemProps: jest.fn((props) => ({ ...props })),
|
|
48
|
-
isListbox: false,
|
|
49
|
-
};
|
|
50
|
-
const testMenuToggleElement = <MenuToggleButton>Open</MenuToggleButton>;
|
|
51
42
|
|
|
52
43
|
const INTERACTION_TYPES = {
|
|
53
44
|
click: {
|
|
@@ -108,6 +99,12 @@ describe("MenuItem when not isListbox", () => {
|
|
|
108
99
|
describe("is clickable", () => {
|
|
109
100
|
it("fires onClick event when clicked and not disabled", async () => {
|
|
110
101
|
const handleClick = jest.fn();
|
|
102
|
+
|
|
103
|
+
const testMenuContext = {
|
|
104
|
+
isOpen: true,
|
|
105
|
+
isListbox: false,
|
|
106
|
+
};
|
|
107
|
+
|
|
111
108
|
const { user } = render(
|
|
112
109
|
<TestWithMenuRoot
|
|
113
110
|
menuToggleElement={testMenuToggleElement}
|
|
@@ -129,9 +129,8 @@ export const MenuRoot = ({
|
|
|
129
129
|
openMenu,
|
|
130
130
|
closeMenu,
|
|
131
131
|
inputValue,
|
|
132
|
-
hiddenItemsCount = 0,
|
|
133
|
-
items,
|
|
134
132
|
getMenuProps,
|
|
133
|
+
autoCloseOnEmpty = false,
|
|
135
134
|
} = contextProps;
|
|
136
135
|
|
|
137
136
|
const toggleElementRef = React.useRef<HTMLElement>(null);
|
|
@@ -155,6 +154,14 @@ export const MenuRoot = ({
|
|
|
155
154
|
// Hack to suppress error: "downshift: You forgot to call the getMenuProps getter function on your component / element."
|
|
156
155
|
getMenuProps?.({}, { suppressRefError: true });
|
|
157
156
|
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!inputValue || !autoCloseOnEmpty) return;
|
|
159
|
+
|
|
160
|
+
if (!menuContext.hasVisibleItems && isOpen) {
|
|
161
|
+
closeMenu?.();
|
|
162
|
+
}
|
|
163
|
+
}, [menuContext.hasVisibleItems, isOpen, inputValue, autoCloseOnEmpty]);
|
|
164
|
+
|
|
158
165
|
return (
|
|
159
166
|
<MenuPopout
|
|
160
167
|
placement={placement}
|
|
@@ -230,3 +230,47 @@ export const SingleSelectMenuWithSearch: Story = {
|
|
|
230
230
|
);
|
|
231
231
|
},
|
|
232
232
|
};
|
|
233
|
+
|
|
234
|
+
export const SingleSelectMenuWithCustomEmpty: Story = {
|
|
235
|
+
name: "Single Select Menu With Custom Empty State",
|
|
236
|
+
render: ({ inputType, ...args }) => {
|
|
237
|
+
return (
|
|
238
|
+
<SingleSelectMenu
|
|
239
|
+
itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
240
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
241
|
+
onStateChange={(changes) => action("onStateChange")(changes)}
|
|
242
|
+
{...(args as object)}
|
|
243
|
+
>
|
|
244
|
+
<MenuHeader title="Select Book">
|
|
245
|
+
<MenuSearchInput
|
|
246
|
+
id="single-select-search"
|
|
247
|
+
name="search"
|
|
248
|
+
type="search"
|
|
249
|
+
aria-label="Search Books"
|
|
250
|
+
/>
|
|
251
|
+
</MenuHeader>
|
|
252
|
+
<MenuContent
|
|
253
|
+
emptyStateElement={
|
|
254
|
+
<div
|
|
255
|
+
style={{
|
|
256
|
+
color: "red",
|
|
257
|
+
fontStyle: "italic",
|
|
258
|
+
padding: "12px",
|
|
259
|
+
fontSize: "14px",
|
|
260
|
+
textAlign: "center",
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
No results found. Try something else...
|
|
264
|
+
</div>
|
|
265
|
+
}
|
|
266
|
+
>
|
|
267
|
+
{TEST_BOOKS.map(({ id, title }) => (
|
|
268
|
+
<MenuItem key={id} id={id} inputType={inputType}>
|
|
269
|
+
{title}
|
|
270
|
+
</MenuItem>
|
|
271
|
+
))}
|
|
272
|
+
</MenuContent>
|
|
273
|
+
</SingleSelectMenu>
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
};
|
package/src/menuTestingUtils.tsx
CHANGED
|
@@ -2,14 +2,15 @@ import React from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
MenuContentContext,
|
|
4
4
|
MenuRoot,
|
|
5
|
-
checkIfItemSelected,
|
|
6
|
-
getSelectedItemIds,
|
|
7
5
|
useMenu,
|
|
8
6
|
type TypeMenuContextProps,
|
|
9
7
|
type TypeMenuContext,
|
|
10
8
|
type TypeMenuRootProps,
|
|
9
|
+
MenuToggleButton,
|
|
11
10
|
} from "./index";
|
|
12
11
|
|
|
12
|
+
export const testMenuToggleElement = <MenuToggleButton>Open</MenuToggleButton>;
|
|
13
|
+
|
|
13
14
|
export const testDefaultContext = ({
|
|
14
15
|
getToggleButtonProps,
|
|
15
16
|
...context
|
|
@@ -24,14 +25,18 @@ export const testDefaultContext = ({
|
|
|
24
25
|
// TODO: Make type on MenuContentContext more agnostic for other use cases
|
|
25
26
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
26
27
|
// @ts-expect-error - mocked function does not match real type
|
|
27
|
-
getItemProps: (props) => ({ role: "option", ...props }),
|
|
28
|
+
getItemProps: jest.fn((props) => ({ role: "option", ...props })),
|
|
28
29
|
// @ts-expect-error - mocked function does not match real type
|
|
29
|
-
getToggleButtonProps: ({ ref, refKey, ...props } = {}) => ({
|
|
30
|
+
getToggleButtonProps: jest.fn(({ ref, refKey = "ref", ...props } = {}) => ({
|
|
31
|
+
onKeyDown: jest.fn(),
|
|
30
32
|
...getToggleButtonProps?.({ ref, refKey, ...props }),
|
|
31
33
|
...(ref ? { [refKey]: ref } : {}),
|
|
32
34
|
...props,
|
|
33
|
-
}),
|
|
35
|
+
})),
|
|
36
|
+
// @ts-expect-error - mocked function does not match real type
|
|
37
|
+
getMenuProps: jest.fn((props) => props),
|
|
34
38
|
isListbox: true,
|
|
39
|
+
hasVisibleItems: true,
|
|
35
40
|
...context,
|
|
36
41
|
});
|
|
37
42
|
|
|
@@ -45,11 +50,11 @@ export const testDefaultContext = ({
|
|
|
45
50
|
*/
|
|
46
51
|
export const TestWithMenuRoot = ({
|
|
47
52
|
children,
|
|
48
|
-
menuToggleElement,
|
|
53
|
+
menuToggleElement = testMenuToggleElement,
|
|
49
54
|
...props
|
|
50
55
|
}: {
|
|
51
56
|
children: TypeMenuRootProps["children"];
|
|
52
|
-
menuToggleElement
|
|
57
|
+
menuToggleElement?: TypeMenuRootProps["menuToggleElement"];
|
|
53
58
|
} & Partial<Omit<TypeMenuRootProps, "children" | "menuToggleElement">>) => {
|
|
54
59
|
return (
|
|
55
60
|
<MenuRoot
|