@radix-ng/primitives 1.0.1 → 1.0.2

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 (37) hide show
  1. package/composite/README.md +1 -1
  2. package/fesm2022/radix-ng-primitives-accordion.mjs +10 -10
  3. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  4. package/fesm2022/radix-ng-primitives-checkbox.mjs +134 -58
  5. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  6. package/fesm2022/radix-ng-primitives-composite.mjs +127 -43
  7. package/fesm2022/radix-ng-primitives-composite.mjs.map +1 -1
  8. package/fesm2022/radix-ng-primitives-menu.mjs +288 -63
  9. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-menubar.mjs +24 -1
  11. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  12. package/fesm2022/radix-ng-primitives-select.mjs +56 -29
  13. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  14. package/fesm2022/radix-ng-primitives-slider.mjs +57 -13
  15. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  16. package/fesm2022/radix-ng-primitives-tabs.mjs +292 -59
  17. package/fesm2022/radix-ng-primitives-tabs.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-toolbar.mjs +19 -13
  19. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  20. package/package.json +2 -10
  21. package/types/radix-ng-primitives-accordion.d.ts +4 -4
  22. package/types/radix-ng-primitives-checkbox.d.ts +98 -70
  23. package/types/radix-ng-primitives-composite.d.ts +58 -15
  24. package/types/radix-ng-primitives-menu.d.ts +44 -16
  25. package/types/radix-ng-primitives-menubar.d.ts +2 -0
  26. package/types/radix-ng-primitives-select.d.ts +46 -32
  27. package/types/radix-ng-primitives-slider.d.ts +19 -4
  28. package/types/radix-ng-primitives-tabs.d.ts +63 -11
  29. package/types/radix-ng-primitives-toolbar.d.ts +80 -73
  30. package/collection/README.md +0 -1
  31. package/fesm2022/radix-ng-primitives-collection.mjs +0 -72
  32. package/fesm2022/radix-ng-primitives-collection.mjs.map +0 -1
  33. package/fesm2022/radix-ng-primitives-roving-focus.mjs +0 -420
  34. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +0 -1
  35. package/roving-focus/README.md +0 -3
  36. package/types/radix-ng-primitives-collection.d.ts +0 -44
  37. package/types/radix-ng-primitives-roving-focus.d.ts +0 -201
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { inject, DestroyRef, signal, effect, afterNextRender, untracked, Directive, ElementRef, input, booleanAttribute, computed, model, output, NgModule } from '@angular/core';
3
3
  import { createContext, useTransitionStatus, injectId, createCancelableChangeEventDetails } from '@radix-ng/primitives/core';
4
4
  import * as i1 from '@radix-ng/primitives/composite';
5
- import { RdxCompositeRoot, RdxCompositeItem } from '@radix-ng/primitives/composite';
5
+ import { RdxCompositeRoot, RdxCompositeListItem, RdxCompositeList, RdxCompositeItem } from '@radix-ng/primitives/composite';
6
6
  import * as i1$1 from '@radix-ng/primitives/presence';
7
7
  import { provideRdxPresenceContext, RdxPresenceDirective } from '@radix-ng/primitives/presence';
8
8
 
@@ -130,7 +130,16 @@ class RdxTabsList {
130
130
  * @default true
131
131
  */
132
132
  this.loopFocus = input(true, { ...(ngDevMode ? { debugName: "loopFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
133
- this.tabMetadata = computed(() => Array.from(this.compositeRoot.itemMap().values()).filter(isTabsTabMetadata), ...(ngDevMode ? [{ debugName: "tabMetadata" }] : /* istanbul ignore next */ []));
133
+ this.tabMap = computed(() => {
134
+ const map = new Map();
135
+ this.compositeRoot.itemMap().forEach((metadata, element) => {
136
+ if (isTabsTabMetadata(metadata)) {
137
+ map.set(element, metadata);
138
+ }
139
+ });
140
+ return map;
141
+ }, ...(ngDevMode ? [{ debugName: "tabMap" }] : /* istanbul ignore next */ []));
142
+ this.tabMetadata = computed(() => Array.from(this.tabMap().values()), ...(ngDevMode ? [{ debugName: "tabMetadata" }] : /* istanbul ignore next */ []));
134
143
  this.disabledIndices = computed(() => this.tabMetadata()
135
144
  .filter((metadata) => metadata.disabled)
136
145
  .map((metadata) => metadata.index), ...(ngDevMode ? [{ debugName: "disabledIndices" }] : /* istanbul ignore next */ []));
@@ -146,11 +155,14 @@ class RdxTabsList {
146
155
  this.compositeRoot.setEnableHomeAndEndKeys(true);
147
156
  });
148
157
  effect(() => {
149
- this.compositeRoot.setDisabledIndices(this.disabledIndices());
158
+ this.compositeRoot.setDisabledIndices([]);
159
+ });
160
+ effect(() => {
161
+ this.rootContext.setTabMap(this.tabMap());
150
162
  });
151
163
  effect(() => {
152
164
  const activeIndex = this.activeIndex();
153
- if (activeIndex === -1 || this.disabledIndices().includes(activeIndex)) {
165
+ if (activeIndex === -1) {
154
166
  return;
155
167
  }
156
168
  const list = this.elementRef.nativeElement;
@@ -158,12 +170,19 @@ class RdxTabsList {
158
170
  if (activeElement && list.contains(activeElement)) {
159
171
  return;
160
172
  }
173
+ if (this.disabledIndices().includes(activeIndex)) {
174
+ const firstEnabledIndex = this.tabMetadata().find((metadata) => !metadata.disabled)?.index;
175
+ if (firstEnabledIndex !== undefined) {
176
+ this.compositeRoot.setHighlightedIndex(firstEnabledIndex);
177
+ }
178
+ return;
179
+ }
161
180
  this.compositeRoot.setHighlightedIndex(activeIndex);
162
181
  });
163
182
  effect(() => this.rootContext.setActivateOnFocus(this.activateOnFocus()));
164
183
  }
165
184
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsList, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
166
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxTabsList, isStandalone: true, selector: "[rdxTabsList]", inputs: { activateOnFocus: { classPropertyName: "activateOnFocus", publicName: "activateOnFocus", isSignal: true, isRequired: false, transformFunction: null }, loopFocus: { classPropertyName: "loopFocus", publicName: "loopFocus", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tablist" }, properties: { "attr.aria-orientation": "rootContext.orientation()", "attr.data-orientation": "rootContext.orientation()", "attr.data-activation-direction": "rootContext.activationDirection()" } }, exportAs: ["rdxTabsList"], hostDirectives: [{ directive: i1.RdxCompositeRoot }], ngImport: i0 }); }
185
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxTabsList, isStandalone: true, selector: "[rdxTabsList]", inputs: { activateOnFocus: { classPropertyName: "activateOnFocus", publicName: "activateOnFocus", isSignal: true, isRequired: false, transformFunction: null }, loopFocus: { classPropertyName: "loopFocus", publicName: "loopFocus", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tablist" }, properties: { "attr.aria-orientation": "rootContext.orientation() === \"vertical\" ? \"vertical\" : undefined", "attr.data-orientation": "rootContext.orientation()", "attr.data-activation-direction": "rootContext.activationDirection()" } }, exportAs: ["rdxTabsList"], hostDirectives: [{ directive: i1.RdxCompositeRoot }], ngImport: i0 }); }
167
186
  }
168
187
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsList, decorators: [{
169
188
  type: Directive,
@@ -173,7 +192,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
173
192
  hostDirectives: [RdxCompositeRoot],
174
193
  host: {
175
194
  role: 'tablist',
176
- '[attr.aria-orientation]': 'rootContext.orientation()',
195
+ '[attr.aria-orientation]': 'rootContext.orientation() === "vertical" ? "vertical" : undefined',
177
196
  '[attr.data-orientation]': 'rootContext.orientation()',
178
197
  '[attr.data-activation-direction]': 'rootContext.activationDirection()'
179
198
  }
@@ -198,6 +217,7 @@ const panelPresenceContext = () => ({ present: inject(RdxTabsPanel).present });
198
217
  class RdxTabsPanel {
199
218
  constructor() {
200
219
  this.elementRef = inject(ElementRef);
220
+ this.listItem = inject(RdxCompositeListItem, { self: true });
201
221
  this.rootContext = injectTabsRootContext();
202
222
  /**
203
223
  * A unique value that associates the panel with a tab.
@@ -216,7 +236,15 @@ class RdxTabsPanel {
216
236
  /** @ignore */
217
237
  this.panelId = computed(() => makePanelId(this.rootContext.baseId, this.value()), ...(ngDevMode ? [{ debugName: "panelId" }] : /* istanbul ignore next */ []));
218
238
  /** @ignore */
219
- this.tabId = computed(() => makeTabId(this.rootContext.baseId, this.value()), ...(ngDevMode ? [{ debugName: "tabId" }] : /* istanbul ignore next */ []));
239
+ this.tabId = computed(() => {
240
+ const value = this.value();
241
+ for (const tabMetadata of this.rootContext.tabMap().values()) {
242
+ if (tabMetadata.value === value) {
243
+ return tabMetadata.id;
244
+ }
245
+ }
246
+ return makeTabId(this.rootContext.baseId, value);
247
+ }, ...(ngDevMode ? [{ debugName: "tabId" }] : /* istanbul ignore next */ []));
220
248
  /** Whether this panel's tab is currently selected. */
221
249
  this.active = computed(() => this.rootContext.value() === this.value(), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
222
250
  /** `true` once a `*rdxTabsPanelPresence` child takes over mounting. */
@@ -233,20 +261,18 @@ class RdxTabsPanel {
233
261
  * element renders nothing), unless `keepMounted` keeps the inactive contents around.
234
262
  */
235
263
  this.hidden = computed(() => !this.active() && this.transitionStatus() !== 'ending' && (!this.hasPresence() || this.keepMounted()), ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
236
- /** @ignore Index of the panel, derived from the order of its associated tab. */
237
- this.index = computed(() => {
238
- const list = this.rootContext.tabListElement();
239
- if (!list) {
240
- return null;
241
- }
242
- const tabs = Array.from(list.querySelectorAll('[role="tab"]'));
243
- const position = tabs.findIndex((tab) => tab.id === makeTabId(this.rootContext.baseId, this.value()));
244
- return position === -1 ? null : position;
245
- }, ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
264
+ /** @ignore Index of the panel in DOM order. */
265
+ this.index = this.listItem.index;
246
266
  this.previousActive = false;
247
267
  this.isFirstRun = true;
248
268
  const unregister = this.transition.registerElement(this.elementRef.nativeElement);
249
269
  inject(DestroyRef).onDestroy(unregister);
270
+ effect(() => {
271
+ this.listItem.setMetadata({
272
+ id: this.panelId(),
273
+ value: this.value()
274
+ });
275
+ });
250
276
  effect(() => {
251
277
  const active = this.active();
252
278
  // Settle the initial state without playing an enter transition.
@@ -266,7 +292,7 @@ class RdxTabsPanel {
266
292
  this.hasPresence.set(true);
267
293
  }
268
294
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsPanel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
269
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxTabsPanel, isStandalone: true, selector: "[rdxTabsPanel]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, keepMounted: { classPropertyName: "keepMounted", publicName: "keepMounted", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tabpanel" }, properties: { "attr.id": "panelId()", "attr.tabindex": "active() ? 0 : undefined", "attr.aria-labelledby": "tabId()", "attr.data-orientation": "rootContext.orientation()", "attr.data-activation-direction": "rootContext.activationDirection()", "attr.data-index": "index()", "attr.data-hidden": "active() ? undefined : \"\"", "attr.data-starting-style": "transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "transitionStatus() === \"ending\" ? \"\" : undefined", "hidden": "hidden()" } }, providers: [provideRdxPresenceContext(panelPresenceContext)], exportAs: ["rdxTabsPanel"], ngImport: i0 }); }
295
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxTabsPanel, isStandalone: true, selector: "[rdxTabsPanel]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, keepMounted: { classPropertyName: "keepMounted", publicName: "keepMounted", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tabpanel" }, properties: { "attr.id": "panelId()", "attr.tabindex": "active() ? 0 : -1", "attr.aria-labelledby": "tabId()", "attr.data-orientation": "rootContext.orientation()", "attr.data-activation-direction": "rootContext.activationDirection()", "attr.data-index": "index()", "attr.data-hidden": "active() ? undefined : \"\"", "attr.data-starting-style": "transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "transitionStatus() === \"ending\" ? \"\" : undefined", "hidden": "hidden()" } }, providers: [provideRdxPresenceContext(panelPresenceContext)], exportAs: ["rdxTabsPanel"], hostDirectives: [{ directive: i1.RdxCompositeListItem }], ngImport: i0 }); }
270
296
  }
271
297
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsPanel, decorators: [{
272
298
  type: Directive,
@@ -274,10 +300,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
274
300
  selector: '[rdxTabsPanel]',
275
301
  exportAs: 'rdxTabsPanel',
276
302
  providers: [provideRdxPresenceContext(panelPresenceContext)],
303
+ hostDirectives: [RdxCompositeListItem],
277
304
  host: {
278
305
  role: 'tabpanel',
279
306
  '[attr.id]': 'panelId()',
280
- '[attr.tabindex]': 'active() ? 0 : undefined',
307
+ '[attr.tabindex]': 'active() ? 0 : -1',
281
308
  '[attr.aria-labelledby]': 'tabId()',
282
309
  '[attr.data-orientation]': 'rootContext.orientation()',
283
310
  '[attr.data-activation-direction]': 'rootContext.activationDirection()',
@@ -322,9 +349,11 @@ const rootContext = () => {
322
349
  activationDirection: root.activationDirection.asReadonly(),
323
350
  activateOnFocus: root.activateOnFocus.asReadonly(),
324
351
  tabListElement: root.tabListElement.asReadonly(),
352
+ tabMap: root.tabMap.asReadonly(),
325
353
  setValue: (value, event, reason) => root.setValue(value, event, reason),
326
354
  setActivateOnFocus: (value) => root.activateOnFocus.set(value),
327
- setTabListElement: (element) => root.tabListElement.set(element)
355
+ setTabListElement: (element) => root.tabListElement.set(element),
356
+ setTabMap: (map) => root.tabMap.set(map)
328
357
  };
329
358
  };
330
359
  /**
@@ -342,8 +371,11 @@ class RdxTabsRoot {
342
371
  this.value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
343
372
  /**
344
373
  * The value of the tab that should be initially selected when uncontrolled.
374
+ * When omitted, Base UI parity uses `0` as the implicit default and falls back to the first enabled tab.
375
+ *
376
+ * @default 0
345
377
  */
346
- this.defaultValue = input(...(ngDevMode ? [undefined, { debugName: "defaultValue" }] : /* istanbul ignore next */ []));
378
+ this.defaultValue = input(undefined, ...(ngDevMode ? [{ debugName: "defaultValue" }] : /* istanbul ignore next */ []));
347
379
  /**
348
380
  * The orientation the tabs are laid out. Controls arrow-key navigation
349
381
  * (left/right vs. up/down).
@@ -353,55 +385,146 @@ class RdxTabsRoot {
353
385
  this.orientation = input('horizontal', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
354
386
  /**
355
387
  * Event emitted when the selected tab changes.
388
+ *
389
+ * `eventDetails.reason` is `'none'` for user-initiated changes, `'initial'` for the first automatic
390
+ * uncontrolled selection, `'disabled'` when an uncontrolled selection falls back from a disabled tab,
391
+ * and `'missing'` when it falls back from a removed tab. Automatic changes are not cancelable.
356
392
  */
357
393
  this.onValueChange = output();
358
394
  /** @ignore Set by `[rdxTabsList]`. */
359
395
  this.activateOnFocus = signal(false, ...(ngDevMode ? [{ debugName: "activateOnFocus" }] : /* istanbul ignore next */ []));
360
396
  /** @ignore Set by `[rdxTabsList]`. */
361
397
  this.tabListElement = signal(null, ...(ngDevMode ? [{ debugName: "tabListElement" }] : /* istanbul ignore next */ []));
398
+ /** @ignore Set by `[rdxTabsList]`. */
399
+ this.tabMap = signal(new Map(), ...(ngDevMode ? [{ debugName: "tabMap" }] : /* istanbul ignore next */ []));
362
400
  /** @ignore */
363
401
  this.activationDirection = signal('none', ...(ngDevMode ? [{ debugName: "activationDirection" }] : /* istanbul ignore next */ []));
402
+ this.externallyControlled = signal(false, ...(ngDevMode ? [{ debugName: "externallyControlled" }] : /* istanbul ignore next */ []));
403
+ this.hasObservedValue = false;
404
+ this.internalValueCommit = false;
405
+ this.hasAppliedDefaultValue = false;
406
+ this.shouldNotifyInitialValueChange = true;
407
+ this.shouldHonorDisabledDefaultValue = false;
408
+ this.didRegisterTabs = false;
364
409
  effect(() => {
365
- const initial = this.defaultValue();
366
- if (initial !== undefined && untracked(this.value) === undefined) {
367
- this.value.set(initial);
410
+ const currentValue = this.value();
411
+ if (this.internalValueCommit) {
412
+ this.internalValueCommit = false;
413
+ this.hasObservedValue = true;
414
+ this.previousObservedValue = currentValue;
415
+ return;
416
+ }
417
+ if (!this.hasObservedValue) {
418
+ this.hasObservedValue = true;
419
+ this.previousObservedValue = currentValue;
420
+ if (currentValue !== undefined) {
421
+ this.externallyControlled.set(true);
422
+ }
423
+ return;
424
+ }
425
+ if (currentValue !== this.previousObservedValue) {
426
+ this.externallyControlled.set(true);
427
+ this.previousObservedValue = currentValue;
428
+ }
429
+ });
430
+ effect(() => {
431
+ if (this.hasAppliedDefaultValue || untracked(this.value) !== undefined) {
432
+ return;
433
+ }
434
+ const defaultValue = this.defaultValue();
435
+ const hasExplicitDefaultValue = defaultValue !== undefined;
436
+ this.hasAppliedDefaultValue = true;
437
+ this.initialDefaultValue = defaultValue ?? 0;
438
+ this.shouldNotifyInitialValueChange = !hasExplicitDefaultValue;
439
+ this.shouldHonorDisabledDefaultValue = hasExplicitDefaultValue;
440
+ untracked(() => this.commitValue(this.initialDefaultValue));
441
+ });
442
+ effect(() => {
443
+ const tabMap = this.tabMap();
444
+ const value = this.value();
445
+ if (this.externallyControlled()) {
446
+ return;
447
+ }
448
+ if (tabMap.size === 0) {
449
+ if (this.didRegisterTabs && value !== null && !this.lastKnownTabElement?.isConnected) {
450
+ untracked(() => this.commitAutomaticValueChange(null, 'missing'));
451
+ }
452
+ return;
453
+ }
454
+ this.didRegisterTabs = true;
455
+ this.lastKnownTabElement = tabMap.keys().next().value;
456
+ const selectedTabMetadata = getTabMetadataByValue(tabMap, value);
457
+ const firstEnabledTabValue = getFirstEnabledTabValue(tabMap);
458
+ const selectionIsDisabled = selectedTabMetadata?.disabled;
459
+ const selectionIsMissing = selectedTabMetadata == null && value !== null;
460
+ if (!selectionIsDisabled && value === this.initialDefaultValue) {
461
+ this.shouldHonorDisabledDefaultValue = false;
462
+ }
463
+ if (this.shouldHonorDisabledDefaultValue && selectionIsDisabled && value === this.initialDefaultValue) {
464
+ return;
465
+ }
466
+ const shouldNotifyInitialValueChange = this.shouldNotifyInitialValueChange;
467
+ if (selectionIsDisabled || selectionIsMissing) {
468
+ const fallbackValue = firstEnabledTabValue ?? null;
469
+ if (value === fallbackValue) {
470
+ this.shouldNotifyInitialValueChange = false;
471
+ return;
472
+ }
473
+ let fallbackReason = 'missing';
474
+ if (shouldNotifyInitialValueChange) {
475
+ fallbackReason = 'initial';
476
+ }
477
+ else if (selectionIsDisabled) {
478
+ fallbackReason = 'disabled';
479
+ }
480
+ untracked(() => this.commitAutomaticValueChange(fallbackValue, fallbackReason));
481
+ return;
482
+ }
483
+ if (shouldNotifyInitialValueChange && selectedTabMetadata != null) {
484
+ untracked(() => this.notifyAutomaticValueChange(value, 'initial'));
485
+ this.shouldNotifyInitialValueChange = false;
368
486
  }
369
487
  });
370
488
  }
371
489
  /** @ignore */
372
- setValue(value, event, reason = event ? 'trigger-press' : 'none') {
490
+ setValue(value, event, reason = 'none') {
373
491
  const previous = this.value();
374
492
  if (previous === value) {
375
493
  return;
376
494
  }
377
495
  const trigger = event?.currentTarget instanceof HTMLElement ? event.currentTarget : undefined;
378
- const { eventDetails } = createCancelableChangeEventDetails(reason, event ?? new Event('tabs.value-change'), trigger);
496
+ const { eventDetails: baseEventDetails } = createCancelableChangeEventDetails(reason, event ?? new Event('tabs.value-change'), trigger);
497
+ const eventDetails = baseEventDetails;
498
+ const activationDirection = computeActivationDirection(previous, value, this.orientation(), this.tabMap());
499
+ eventDetails.activationDirection = activationDirection;
379
500
  this.onValueChange.emit({ value, eventDetails });
380
501
  if (eventDetails.isCanceled()) {
381
502
  return;
382
503
  }
383
- this.activationDirection.set(this.computeDirection(previous, value));
504
+ this.activationDirection.set(activationDirection);
505
+ this.commitValue(value);
506
+ }
507
+ commitValue(value) {
508
+ this.internalValueCommit = true;
384
509
  this.value.set(value);
385
510
  }
386
- computeDirection(previous, next) {
387
- const list = this.tabListElement();
388
- if (!list || previous === undefined || previous === null) {
389
- return 'none';
390
- }
391
- const tabs = Array.from(list.querySelectorAll('[role="tab"]'));
392
- const previousIndex = tabs.findIndex((tab) => tab.id === makeTabId(this.baseId, previous));
393
- const nextIndex = tabs.findIndex((tab) => tab.id === makeTabId(this.baseId, next));
394
- if (previousIndex === -1 || nextIndex === -1 || previousIndex === nextIndex) {
395
- return 'none';
396
- }
397
- const horizontal = this.orientation() === 'horizontal';
398
- if (nextIndex > previousIndex) {
399
- return horizontal ? 'right' : 'down';
511
+ commitAutomaticValueChange(value, reason) {
512
+ this.activationDirection.set('none');
513
+ this.commitValue(value);
514
+ this.notifyAutomaticValueChange(value, reason);
515
+ this.shouldNotifyInitialValueChange = false;
516
+ }
517
+ notifyAutomaticValueChange(value, reason) {
518
+ if (value === undefined) {
519
+ return;
400
520
  }
401
- return horizontal ? 'left' : 'up';
521
+ const { eventDetails: baseEventDetails } = createCancelableChangeEventDetails(reason, new Event('tabs.value-change'));
522
+ const eventDetails = baseEventDetails;
523
+ eventDetails.activationDirection = 'none';
524
+ this.onValueChange.emit({ value, eventDetails });
402
525
  }
403
526
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
404
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxTabsRoot, isStandalone: true, selector: "[rdxTabsRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onValueChange: "onValueChange" }, host: { properties: { "attr.data-orientation": "orientation()", "attr.data-activation-direction": "activationDirection()" } }, providers: [provideTabsRootContext(rootContext)], exportAs: ["rdxTabsRoot"], ngImport: i0 }); }
527
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxTabsRoot, isStandalone: true, selector: "[rdxTabsRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onValueChange: "onValueChange" }, host: { properties: { "attr.data-orientation": "orientation()", "attr.data-activation-direction": "activationDirection()" } }, providers: [provideTabsRootContext(rootContext)], exportAs: ["rdxTabsRoot"], hostDirectives: [{ directive: i1.RdxCompositeList }], ngImport: i0 }); }
405
528
  }
406
529
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsRoot, decorators: [{
407
530
  type: Directive,
@@ -409,12 +532,87 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
409
532
  selector: '[rdxTabsRoot]',
410
533
  exportAs: 'rdxTabsRoot',
411
534
  providers: [provideTabsRootContext(rootContext)],
535
+ hostDirectives: [RdxCompositeList],
412
536
  host: {
413
537
  '[attr.data-orientation]': 'orientation()',
414
538
  '[attr.data-activation-direction]': 'activationDirection()'
415
539
  }
416
540
  }]
417
541
  }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }] } });
542
+ function getTabMetadataByValue(tabMap, value) {
543
+ for (const tabMetadata of tabMap.values()) {
544
+ if (tabMetadata.value === value) {
545
+ return tabMetadata;
546
+ }
547
+ }
548
+ return undefined;
549
+ }
550
+ function getFirstEnabledTabValue(tabMap) {
551
+ for (const tabMetadata of tabMap.values()) {
552
+ if (!tabMetadata.disabled) {
553
+ return tabMetadata.value;
554
+ }
555
+ }
556
+ return undefined;
557
+ }
558
+ function computeActivationDirection(previous, next, orientation, tabMap) {
559
+ if (previous == null || next == null) {
560
+ return 'none';
561
+ }
562
+ let previousTab = null;
563
+ let nextTab = null;
564
+ let previousIndex = -1;
565
+ let nextIndex = -1;
566
+ for (const [tabElement, tabMetadata] of tabMap.entries()) {
567
+ if (tabMetadata.value === previous) {
568
+ previousTab = tabElement;
569
+ previousIndex = tabMetadata.index;
570
+ }
571
+ if (tabMetadata.value === next) {
572
+ nextTab = tabElement;
573
+ nextIndex = tabMetadata.index;
574
+ }
575
+ if (previousTab && nextTab) {
576
+ break;
577
+ }
578
+ }
579
+ if (!previousTab || !nextTab || previousIndex === nextIndex) {
580
+ return inferActivationDirectionFromValues(previous, next, orientation);
581
+ }
582
+ const previousRect = previousTab.getBoundingClientRect();
583
+ const nextRect = nextTab.getBoundingClientRect();
584
+ if (orientation === 'horizontal') {
585
+ if (nextRect.left < previousRect.left) {
586
+ return 'left';
587
+ }
588
+ if (nextRect.left > previousRect.left) {
589
+ return 'right';
590
+ }
591
+ return nextIndex > previousIndex ? 'right' : 'left';
592
+ }
593
+ if (nextRect.top < previousRect.top) {
594
+ return 'up';
595
+ }
596
+ if (nextRect.top > previousRect.top) {
597
+ return 'down';
598
+ }
599
+ return nextIndex > previousIndex ? 'down' : 'up';
600
+ }
601
+ function inferActivationDirectionFromValues(previous, next, orientation) {
602
+ if (previous !== next && typeof previous === 'number' && typeof next === 'number') {
603
+ if (orientation === 'horizontal') {
604
+ return next > previous ? 'right' : 'left';
605
+ }
606
+ return next > previous ? 'down' : 'up';
607
+ }
608
+ if (previous !== next && typeof previous === 'string' && typeof next === 'string') {
609
+ if (orientation === 'horizontal') {
610
+ return next > previous ? 'right' : 'left';
611
+ }
612
+ return next > previous ? 'down' : 'up';
613
+ }
614
+ return 'none';
615
+ }
418
616
 
419
617
  /**
420
618
  * An individual interactive tab button that activates its corresponding panel.
@@ -431,14 +629,27 @@ class RdxTabsTab {
431
629
  this.value = input.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
432
630
  /**
433
631
  * When `true`, prevents the user from interacting with the tab.
632
+ * Disabled tabs remain focusable during composite keyboard navigation, matching Base UI.
434
633
  */
435
634
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
635
+ /**
636
+ * Whether the host element is a native button. When `true`, `type="button"` is applied.
637
+ *
638
+ * @default true
639
+ */
640
+ this.nativeButton = input(true, { ...(ngDevMode ? { debugName: "nativeButton" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
641
+ /**
642
+ * Optional id for the tab element. When omitted, an id is derived from the root id and tab value.
643
+ */
644
+ this.id = input(undefined, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
436
645
  /** @ignore */
437
- this.tabId = computed(() => makeTabId(this.rootContext.baseId, this.value()), ...(ngDevMode ? [{ debugName: "tabId" }] : /* istanbul ignore next */ []));
646
+ this.tabId = computed(() => this.id() ?? makeTabId(this.rootContext.baseId, this.value()), ...(ngDevMode ? [{ debugName: "tabId" }] : /* istanbul ignore next */ []));
438
647
  /** @ignore */
439
648
  this.panelId = computed(() => makePanelId(this.rootContext.baseId, this.value()), ...(ngDevMode ? [{ debugName: "panelId" }] : /* istanbul ignore next */ []));
440
649
  /** @ignore */
441
650
  this.active = computed(() => this.rootContext.value() === this.value(), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
651
+ this.isPressing = false;
652
+ this.isMainButton = false;
442
653
  effect(() => {
443
654
  this.compositeItem.setMetadata({
444
655
  disabled: this.disabled(),
@@ -448,30 +659,50 @@ class RdxTabsTab {
448
659
  });
449
660
  }
450
661
  /** @ignore */
451
- onMouseDown(event) {
452
- // Only the primary button selects; ignore Ctrl-click (macOS right-click emulation).
453
- if (!this.disabled() && event.button === 0 && !event.ctrlKey) {
454
- this.rootContext.setValue(this.value(), event, 'trigger-press');
455
- }
456
- else {
457
- // Prevent focus to avoid accidental activation.
458
- event.preventDefault();
662
+ onClick(event) {
663
+ if (this.active() || this.disabled()) {
664
+ return;
459
665
  }
666
+ this.rootContext.setValue(this.value(), event, 'none');
460
667
  }
461
668
  /** @ignore */
462
669
  onKeyDown(event) {
463
670
  if (!this.disabled() && (event.key === ' ' || event.key === 'Enter')) {
464
- this.rootContext.setValue(this.value(), event, 'keyboard');
671
+ this.rootContext.setValue(this.value(), event, 'none');
465
672
  }
466
673
  }
467
674
  /** @ignore */
675
+ onPointerDown(event) {
676
+ if (this.active() || this.disabled()) {
677
+ return;
678
+ }
679
+ this.isPressing = true;
680
+ this.isMainButton = event.button === 0;
681
+ const ownerDocument = event.currentTarget instanceof HTMLElement ? event.currentTarget.ownerDocument : document;
682
+ ownerDocument.addEventListener('pointerup', () => {
683
+ this.isPressing = false;
684
+ this.isMainButton = false;
685
+ }, { once: true });
686
+ ownerDocument.addEventListener('pointercancel', () => {
687
+ this.isPressing = false;
688
+ this.isMainButton = false;
689
+ }, { once: true });
690
+ ownerDocument.addEventListener('blur', () => {
691
+ this.isPressing = false;
692
+ this.isMainButton = false;
693
+ }, { once: true });
694
+ }
695
+ /** @ignore */
468
696
  onFocus(event) {
469
- if (!this.active() && !this.disabled() && this.rootContext.activateOnFocus()) {
470
- this.rootContext.setValue(this.value(), event, 'focus');
697
+ if (this.active() || this.disabled()) {
698
+ return;
699
+ }
700
+ if (this.rootContext.activateOnFocus() && (!this.isPressing || this.isMainButton)) {
701
+ this.rootContext.setValue(this.value(), event, 'none');
471
702
  }
472
703
  }
473
704
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsTab, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
474
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxTabsTab, isStandalone: true, selector: "[rdxTabsTab]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button", "role": "tab" }, listeners: { "mousedown": "onMouseDown($event)", "keydown": "onKeyDown($event)", "focus": "onFocus($event)" }, properties: { "attr.id": "tabId()", "attr.aria-selected": "active()", "attr.aria-controls": "panelId()", "attr.aria-disabled": "disabled() ? \"true\" : undefined", "attr.data-composite-item-active": "active() ? \"\" : undefined", "attr.data-orientation": "rootContext.orientation()", "attr.data-activation-direction": "rootContext.activationDirection()", "attr.data-active": "active() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined" } }, exportAs: ["rdxTabsTab"], hostDirectives: [{ directive: i1.RdxCompositeItem }], ngImport: i0 }); }
705
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxTabsTab, isStandalone: true, selector: "[rdxTabsTab]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, nativeButton: { classPropertyName: "nativeButton", publicName: "nativeButton", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tab" }, listeners: { "click": "onClick($event)", "keydown": "onKeyDown($event)", "pointerdown": "onPointerDown($event)", "focus": "onFocus($event)" }, properties: { "attr.type": "nativeButton() ? \"button\" : undefined", "attr.id": "tabId()", "attr.aria-selected": "active()", "attr.aria-controls": "panelId()", "attr.aria-disabled": "disabled() ? \"true\" : undefined", "attr.disabled": "null", "attr.data-composite-item-active": "active() ? \"\" : undefined", "attr.data-orientation": "rootContext.orientation()", "attr.data-activation-direction": "rootContext.activationDirection()", "attr.data-active": "active() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined" } }, exportAs: ["rdxTabsTab"], hostDirectives: [{ directive: i1.RdxCompositeItem }], ngImport: i0 }); }
475
706
  }
476
707
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsTab, decorators: [{
477
708
  type: Directive,
@@ -480,23 +711,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
480
711
  exportAs: 'rdxTabsTab',
481
712
  hostDirectives: [RdxCompositeItem],
482
713
  host: {
483
- type: 'button',
714
+ '[attr.type]': 'nativeButton() ? "button" : undefined',
484
715
  role: 'tab',
485
716
  '[attr.id]': 'tabId()',
486
717
  '[attr.aria-selected]': 'active()',
487
718
  '[attr.aria-controls]': 'panelId()',
488
719
  '[attr.aria-disabled]': 'disabled() ? "true" : undefined',
720
+ '[attr.disabled]': 'null',
489
721
  '[attr.data-composite-item-active]': 'active() ? "" : undefined',
490
722
  '[attr.data-orientation]': 'rootContext.orientation()',
491
723
  '[attr.data-activation-direction]': 'rootContext.activationDirection()',
492
724
  '[attr.data-active]': 'active() ? "" : undefined',
493
725
  '[attr.data-disabled]': 'disabled() ? "" : undefined',
494
- '(mousedown)': 'onMouseDown($event)',
726
+ '(click)': 'onClick($event)',
495
727
  '(keydown)': 'onKeyDown($event)',
728
+ '(pointerdown)': 'onPointerDown($event)',
496
729
  '(focus)': 'onFocus($event)'
497
730
  }
498
731
  }]
499
- }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
732
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], nativeButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "nativeButton", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }] } });
500
733
 
501
734
  const tabsImports = [RdxTabsRoot, RdxTabsList, RdxTabsTab, RdxTabsPanel, RdxTabsPanelPresence, RdxTabsIndicator];
502
735
  class RdxTabsModule {