terrier-engine 4.71.8 → 4.72.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/terrier/tabs.ts +116 -17
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.71.8",
7
+ "version": "4.72.1",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
package/terrier/tabs.ts CHANGED
@@ -14,11 +14,13 @@ export type TabParams = {
14
14
  key: string
15
15
  title: string
16
16
  icon?: IconName
17
+ iconColor?: ColorName
17
18
  state?: 'enabled' | 'disabled' | 'hidden'
18
19
  classes?: string[]
19
20
  tabClasses?: string[]
20
21
  click?: Packet
21
- iconColor?: ColorName
22
+ secondaryIcon?: IconName
23
+ secondaryIconColor?: ColorName
22
24
  }
23
25
 
24
26
  /**
@@ -181,26 +183,37 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
181
183
  }
182
184
  parent.div('tt-tab-container', this.state.side, container => {
183
185
  container.div('.tt-flex.tt-tab-list', tabList => {
184
- tabList.class(`tablist-${this.id}`)
185
186
  if (this._beforeActions.length) {
186
187
  this.theme.renderActions(tabList, this._beforeActions, { defaultClass: 'action' })
187
188
  }
188
- for (const tabKey of this.tabOrder) {
189
- const tab = this.tabs.get(tabKey)!
190
- if (tab.state == 'hidden') continue
191
-
192
- tabList.a('.tab', a => {
193
- if (tab.tabClasses?.length) a.class(...tab.tabClasses)
194
- a.attrs({ draggable: this.state.reorderable })
195
- a.data({ key: tab.key })
196
- a.class(tab.state || 'enabled')
197
- if (tab.key === currentTabKey) a.class('active')
198
- if (tab.icon) this.theme.renderIcon(a, tab.icon, tab.iconColor ? tab.iconColor : 'secondary')
199
- a.span({ text: tab.title })
200
- a.emitClick(this.changeTabKey, { tabKey: tab.key })
201
- if (tab.click) a.emitClick(tab.click.key, tab.click.data || {})
189
+ // The tabs live inside a scroll viewport/track so that they can be shifted
190
+ // horizontally (for top/bottom sides) without moving the before/after actions.
191
+ tabList.div('.tt-tab-scroll', scroll => {
192
+ scroll.div('.tt-tab-track', track => {
193
+ track.class(`tablist-${this.id}`)
194
+ for (const tabKey of this.tabOrder) {
195
+ const tab = this.tabs.get(tabKey)!
196
+ if (tab.state == 'hidden') continue
197
+
198
+ track.a('.tab', a => {
199
+ if (tab.tabClasses?.length) a.class(...tab.tabClasses)
200
+ a.attrs({ draggable: this.state.reorderable })
201
+ a.data({ key: tab.key })
202
+ a.class(tab.state || 'enabled')
203
+ if (tab.key === currentTabKey) a.class('active')
204
+ if (tab.icon) this.theme.renderIcon(a, tab.icon, tab.iconColor ? tab.iconColor : 'secondary')
205
+ a.span({ text: tab.title })
206
+ if (tab.secondaryIcon) {
207
+ a.span('.tt-tab-secondary-icon', span => {
208
+ this.theme.renderIcon(span, tab.secondaryIcon!, tab.secondaryIconColor ? tab.secondaryIconColor : 'secondary')
209
+ })
210
+ }
211
+ a.emitClick(this.changeTabKey, { tabKey: tab.key })
212
+ if (tab.click) a.emitClick(tab.click.key, tab.click.data || {})
213
+ })
214
+ }
202
215
  })
203
- }
216
+ })
204
217
  if (this._afterActions.length) {
205
218
  tabList.div('.spacer')
206
219
  this.theme.renderActions(tabList, this._afterActions, { defaultClass: 'action' })
@@ -217,6 +230,92 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
217
230
  })
218
231
  }
219
232
 
233
+ private tabScrollObserver?: ResizeObserver
234
+
235
+ update(elem: HTMLElement) {
236
+ super.update(elem)
237
+ this.layoutHorizontalTabs()
238
+ }
239
+
240
+ /**
241
+ * For top/bottom tab containers, wires up the horizontal scroll behavior: keeps the selected
242
+ * tab in view, translates vertical mouse-wheel gestures into horizontal scrolling, and keeps
243
+ * the selected tab visible as the container is resized.
244
+ */
245
+ private layoutHorizontalTabs() {
246
+ const container = this.element?.querySelector('.tt-tab-container')
247
+ const isHorizontal = !!container && (container.classList.contains('top') || container.classList.contains('bottom'))
248
+ if (!isHorizontal) {
249
+ this.tabScrollObserver?.disconnect()
250
+ this.tabScrollObserver = undefined
251
+ return
252
+ }
253
+
254
+ const viewport = container!.querySelector('.tt-tab-scroll') as HTMLElement | null
255
+ if (!viewport) return
256
+
257
+ // The viewport is recreated on every full render, so the observer has to be re-pointed and
258
+ // the wheel handler re-attached each time update() runs. The dataset flag prevents attaching
259
+ // the listener more than once when the same element survives a stale update.
260
+ if (!this.tabScrollObserver) {
261
+ this.tabScrollObserver = new ResizeObserver(() => this.applyTabScrollOffset())
262
+ }
263
+ this.tabScrollObserver.disconnect()
264
+ this.tabScrollObserver.observe(viewport)
265
+
266
+ if (!viewport.dataset.wheelBound) {
267
+ viewport.dataset.wheelBound = 'true'
268
+ viewport.addEventListener('wheel', this.onTabWheel, { passive: false })
269
+ }
270
+
271
+ this.applyTabScrollOffset()
272
+ }
273
+
274
+ /**
275
+ * Lets a plain (vertical) mouse wheel scroll the tabs horizontally, so users without a
276
+ * trackpad can still scroll left and right. Only intercepts when the tabs actually overflow.
277
+ */
278
+ private onTabWheel = (e: WheelEvent) => {
279
+ if (e.ctrlKey) return // allow browser zoom
280
+ const viewport = e.currentTarget as HTMLElement
281
+ if (viewport.scrollWidth <= viewport.clientWidth) return // nothing to scroll
282
+ const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY
283
+ if (!delta) return
284
+ viewport.scrollLeft += delta
285
+ e.preventDefault()
286
+ }
287
+
288
+ /**
289
+ * Scrolls the viewport so the selected tab sits near the left edge, offset slightly so the
290
+ * preceding tab remains partially visible. Only scrolls when the tabs actually overflow their
291
+ * container; otherwise they're left in place.
292
+ */
293
+ private applyTabScrollOffset() {
294
+ const viewport = this.element?.querySelector('.tt-tab-scroll') as HTMLElement | null
295
+ const track = this.element?.querySelector('.tt-tab-track') as HTMLElement | null
296
+ if (!viewport || !track) return
297
+
298
+ const active = track.querySelector('.tab.active') as HTMLElement | null
299
+ const contentWidth = track.scrollWidth
300
+ const viewportWidth = viewport.clientWidth
301
+
302
+ // If all the tabs fit (or nothing's selected), don't move them.
303
+ if (!active || contentWidth <= viewportWidth) {
304
+ viewport.scrollLeft = 0
305
+ return
306
+ }
307
+
308
+ // Line up the left edge of the viewport just before the active tab, leaving a sliver of the
309
+ // preceding tab visible. Clamp so we never scroll past the last tab (no empty gap at the end).
310
+ const prev = active.previousElementSibling as HTMLElement | null
311
+ let offset = active.offsetLeft
312
+ if (prev) {
313
+ offset = prev.offsetLeft + prev.offsetWidth - Math.min(prev.offsetWidth, 32)
314
+ }
315
+ const maxOffset = contentWidth - viewportWidth
316
+ viewport.scrollLeft = Math.max(0, Math.min(offset, maxOffset))
317
+ }
318
+
220
319
  }
221
320
 
222
321
  const Tabs = {