Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
giswqs
GitHub Repository: giswqs/geemap
Path: blob/master/js/tab_panel.ts
2313 views
1
import { html, css, nothing, LitElement, PropertyValues } from "lit";
2
import { property, queryAll, queryAssignedElements } from "lit/decorators.js";
3
import { classMap } from "lit/directives/class-map.js";
4
import { styleMap } from "lit/directives/style-map.js";
5
6
import { legacyStyles } from "./ipywidgets_styles";
7
import { materialStyles } from "./styles";
8
9
function convertToId(name: string | undefined): string {
10
return (name || "").trim().replace(" ", "-").toLowerCase();
11
}
12
13
/** The various modes. */
14
export enum TabMode {
15
ALWAYS_SHOW,
16
HIDE_ON_SECOND_CLICK,
17
}
18
19
/** The tab alignment. */
20
export enum Alignment {
21
CENTER = 'center',
22
LEFT = 'left',
23
RIGHT = 'right',
24
}
25
26
/** The tab configuration, as a string or Material Icon. */
27
export interface Tab {
28
name: string | undefined,
29
icon: string | undefined,
30
width: number | undefined,
31
}
32
33
/**
34
* Defines the <tab-panel> element which accepts N children, with a zero-based
35
* index determining which child to show, e.g.:
36
* <tab-panel>
37
* <p>Show when index is 0</p>
38
* <p>Show when index is 1</p>
39
* <p>Show when index is 2</p>
40
* </tab-panel>
41
*/
42
export class TabPanel extends LitElement {
43
static get componentName() {
44
return `tab-panel`;
45
}
46
47
static override styles = [
48
legacyStyles,
49
materialStyles,
50
css`
51
::slotted(*) {
52
display: none;
53
}
54
55
::slotted(.show-tab) {
56
display: block;
57
}
58
59
.container {
60
padding: 0;
61
width: 100%;
62
}
63
64
.tab-container {
65
align-items: center;
66
display: flex;
67
flex-direction: row;
68
}
69
70
.tab-container.center {
71
justify-content: center;
72
}
73
74
.tab-container.left {
75
justify-content: flex-start;
76
}
77
78
.tab-container.right {
79
justify-content: flex-end;
80
}
81
82
.tab-container button {
83
border-radius: 5px;
84
height: 28px;
85
margin: 2px 0 2px 8px;
86
user-select: none;
87
}
88
89
.tab-container button.icon {
90
font-size: 16px;
91
width: 28px;
92
}
93
94
.tab-container button.name {
95
padding: 0 8px;
96
}
97
`,
98
];
99
100
@property({ type: Array })
101
tabs: Tab[] = [];
102
103
@property({ type: Number })
104
index = 0;
105
106
@property({ type: Number })
107
mode: TabMode = TabMode.HIDE_ON_SECOND_CLICK;
108
109
@property({ type: String })
110
alignment: Alignment = Alignment.LEFT;
111
112
/**
113
* The tab elements.
114
*/
115
@queryAll(".tab") tabElements!: HTMLDivElement[];
116
117
/**
118
* The tab content element to show at a given index. Note that child
119
* elements are set to display block or none based on the index, and
120
* top-level text elements are ignored.
121
*/
122
@queryAssignedElements() tabContentElements!: HTMLElement[];
123
124
override render() {
125
return html`
126
<div class="container">
127
<div
128
role="tablist"
129
class="${classMap({
130
"tab-container": true,
131
"center": this.alignment === Alignment.CENTER,
132
"left": this.alignment === Alignment.LEFT,
133
"right": this.alignment === Alignment.RIGHT,
134
})}">
135
${this.renderTabs()}
136
</div>
137
<slot @slotchange=${this.updateSlotChildren}></slot>
138
</div>
139
`;
140
}
141
142
override update(changedProperties: PropertyValues) {
143
super.update(changedProperties);
144
if (changedProperties.has("index") && changedProperties.get("index") != null) {
145
this.updateSlotChildren();
146
}
147
}
148
149
private updateSlotChildren() {
150
if (!this.tabContentElements) {
151
return;
152
}
153
// Show the element at the current index.
154
this.tabContentElements.forEach((element: HTMLElement, i: number) => {
155
element.classList.remove("show-tab");
156
157
// Also add accessibility attributes.
158
const id = convertToId(this.tabs[i].name);
159
element.setAttribute("id", `tabpanel-${id}-${i}`);
160
element.setAttribute("role", "tabpanel");
161
element.setAttribute("aria-labelledby", `tab-${id}-${i}`);
162
});
163
this.tabContentElements[this.index]?.classList.add("show-tab");
164
}
165
166
private renderTabs() {
167
return this.tabs.map((tab: Tab, i: number) => {
168
const id = convertToId(this.tabs[i].name);
169
return html`<button
170
id="tab-${id}-${i}"
171
class="${classMap({
172
"legacy-button": true,
173
"active": i === this.index,
174
"icon": !!tab.icon,
175
"name": !!tab.name,
176
})}"
177
style="${styleMap({
178
width: tab.width ? `${tab.width}px` : null,
179
})}"
180
type="button"
181
role="tab"
182
aria-selected="${i === this.index ? true : false}"
183
aria-controls="tabpanel-${id}-${i}"
184
@click=${() => {
185
this.onTabClick(i);
186
}}>
187
${tab.icon ? html`<span class="material-symbols-outlined">${tab.icon}</span>` : nothing}
188
<span>${tab.name}</span>
189
</button>`;
190
});
191
}
192
193
private onTabClick(index: number) {
194
switch (this.mode) {
195
case TabMode.HIDE_ON_SECOND_CLICK:
196
// Hide the tab panel if clicked twice.
197
this.index = this.index === index ? -1 : index;
198
break;
199
case TabMode.ALWAYS_SHOW:
200
default:
201
this.index = index;
202
}
203
this.dispatchEvent(new CustomEvent("tab-changed", {
204
detail: index,
205
}));
206
}
207
}
208
209
// Without this check, there's a component registry issue when developing locally.
210
if (!customElements.get(TabPanel.componentName)) {
211
customElements.define(TabPanel.componentName, TabPanel);
212
}
213
214