@radix-ng/primitives 1.0.0-beta.5 → 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 (62) hide show
  1. package/composite/README.md +3 -0
  2. package/fesm2022/radix-ng-primitives-accordion.mjs +20 -44
  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 +599 -0
  7. package/fesm2022/radix-ng-primitives-composite.mjs.map +1 -0
  8. package/fesm2022/radix-ng-primitives-drawer.mjs +442 -2
  9. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-menu.mjs +315 -68
  11. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  12. package/fesm2022/radix-ng-primitives-menubar.mjs +91 -36
  13. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  14. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +281 -88
  15. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  16. package/fesm2022/radix-ng-primitives-popover.mjs +40 -15
  17. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-popper.mjs +73 -65
  19. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-radio.mjs +63 -27
  21. package/fesm2022/radix-ng-primitives-radio.mjs.map +1 -1
  22. package/fesm2022/radix-ng-primitives-scroll-area.mjs +56 -25
  23. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  24. package/fesm2022/radix-ng-primitives-select.mjs +59 -29
  25. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  26. package/fesm2022/radix-ng-primitives-slider.mjs +57 -13
  27. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  28. package/fesm2022/radix-ng-primitives-tabs.mjs +335 -73
  29. package/fesm2022/radix-ng-primitives-tabs.mjs.map +1 -1
  30. package/fesm2022/radix-ng-primitives-toggle-group.mjs +66 -21
  31. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  32. package/fesm2022/radix-ng-primitives-toggle.mjs +29 -11
  33. package/fesm2022/radix-ng-primitives-toggle.mjs.map +1 -1
  34. package/fesm2022/radix-ng-primitives-toolbar.mjs +68 -36
  35. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  36. package/navigation-menu/README.md +5 -2
  37. package/package.json +6 -10
  38. package/types/radix-ng-primitives-accordion.d.ts +12 -16
  39. package/types/radix-ng-primitives-checkbox.d.ts +98 -70
  40. package/types/radix-ng-primitives-composite.d.ts +195 -0
  41. package/types/radix-ng-primitives-drawer.d.ts +40 -2
  42. package/types/radix-ng-primitives-menu.d.ts +46 -16
  43. package/types/radix-ng-primitives-menubar.d.ts +12 -5
  44. package/types/radix-ng-primitives-navigation-menu.d.ts +65 -33
  45. package/types/radix-ng-primitives-popover.d.ts +9 -5
  46. package/types/radix-ng-primitives-popper.d.ts +1 -0
  47. package/types/radix-ng-primitives-radio.d.ts +11 -9
  48. package/types/radix-ng-primitives-scroll-area.d.ts +4 -1
  49. package/types/radix-ng-primitives-select.d.ts +46 -32
  50. package/types/radix-ng-primitives-slider.d.ts +19 -4
  51. package/types/radix-ng-primitives-tabs.d.ts +69 -14
  52. package/types/radix-ng-primitives-toggle-group.d.ts +27 -16
  53. package/types/radix-ng-primitives-toggle.d.ts +5 -5
  54. package/types/radix-ng-primitives-toolbar.d.ts +84 -69
  55. package/collection/README.md +0 -1
  56. package/fesm2022/radix-ng-primitives-collection.mjs +0 -72
  57. package/fesm2022/radix-ng-primitives-collection.mjs.map +0 -1
  58. package/fesm2022/radix-ng-primitives-roving-focus.mjs +0 -388
  59. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +0 -1
  60. package/roving-focus/README.md +0 -3
  61. package/types/radix-ng-primitives-collection.d.ts +0 -44
  62. package/types/radix-ng-primitives-roving-focus.d.ts +0 -187
@@ -1,8 +1,8 @@
1
1
  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
- import * as i1 from '@radix-ng/primitives/roving-focus';
5
- import { RdxRovingFocusGroupDirective, RdxRovingFocusItemDirective } from '@radix-ng/primitives/roving-focus';
4
+ import * as i1 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
 
@@ -116,7 +116,7 @@ class RdxTabsList {
116
116
  constructor() {
117
117
  this.rootContext = injectTabsRootContext();
118
118
  this.elementRef = inject(ElementRef);
119
- this.rovingFocusGroup = inject(RdxRovingFocusGroupDirective, { self: true });
119
+ this.compositeRoot = inject(RdxCompositeRoot, { self: true });
120
120
  /**
121
121
  * Whether a tab is activated when it receives focus (automatic activation).
122
122
  * When `false`, tabs are only activated on click or Enter/Space.
@@ -130,30 +130,79 @@ class RdxTabsList {
130
130
  * @default true
131
131
  */
132
132
  this.loopFocus = input(true, { ...(ngDevMode ? { debugName: "loopFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
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 */ []));
143
+ this.disabledIndices = computed(() => this.tabMetadata()
144
+ .filter((metadata) => metadata.disabled)
145
+ .map((metadata) => metadata.index), ...(ngDevMode ? [{ debugName: "disabledIndices" }] : /* istanbul ignore next */ []));
146
+ this.activeIndex = computed(() => {
147
+ const value = this.rootContext.value();
148
+ const metadata = this.tabMetadata().find((tab) => tab.value === value);
149
+ return metadata?.index ?? -1;
150
+ }, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
133
151
  this.rootContext.setTabListElement(this.elementRef.nativeElement);
134
152
  effect(() => {
135
- this.rovingFocusGroup.setOrientation(this.rootContext.orientation());
136
- this.rovingFocusGroup.setLoop(this.loopFocus());
153
+ this.compositeRoot.setOrientation(this.rootContext.orientation());
154
+ this.compositeRoot.setLoopFocus(this.loopFocus());
155
+ this.compositeRoot.setEnableHomeAndEndKeys(true);
156
+ });
157
+ effect(() => {
158
+ this.compositeRoot.setDisabledIndices([]);
159
+ });
160
+ effect(() => {
161
+ this.rootContext.setTabMap(this.tabMap());
162
+ });
163
+ effect(() => {
164
+ const activeIndex = this.activeIndex();
165
+ if (activeIndex === -1) {
166
+ return;
167
+ }
168
+ const list = this.elementRef.nativeElement;
169
+ const activeElement = list.ownerDocument.activeElement;
170
+ if (activeElement && list.contains(activeElement)) {
171
+ return;
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
+ }
180
+ this.compositeRoot.setHighlightedIndex(activeIndex);
137
181
  });
138
182
  effect(() => this.rootContext.setActivateOnFocus(this.activateOnFocus()));
139
183
  }
140
184
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsList, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
141
- 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.RdxRovingFocusGroupDirective }], 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 }); }
142
186
  }
143
187
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsList, decorators: [{
144
188
  type: Directive,
145
189
  args: [{
146
190
  selector: '[rdxTabsList]',
147
191
  exportAs: 'rdxTabsList',
148
- hostDirectives: [RdxRovingFocusGroupDirective],
192
+ hostDirectives: [RdxCompositeRoot],
149
193
  host: {
150
194
  role: 'tablist',
151
- '[attr.aria-orientation]': 'rootContext.orientation()',
195
+ '[attr.aria-orientation]': 'rootContext.orientation() === "vertical" ? "vertical" : undefined',
152
196
  '[attr.data-orientation]': 'rootContext.orientation()',
153
197
  '[attr.data-activation-direction]': 'rootContext.activationDirection()'
154
198
  }
155
199
  }]
156
200
  }], ctorParameters: () => [], propDecorators: { activateOnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "activateOnFocus", required: false }] }], loopFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "loopFocus", required: false }] }] } });
201
+ function isTabsTabMetadata(metadata) {
202
+ return (typeof metadata['disabled'] === 'boolean' &&
203
+ typeof metadata['id'] === 'string' &&
204
+ Object.prototype.hasOwnProperty.call(metadata, 'value'));
205
+ }
157
206
 
158
207
  const panelPresenceContext = () => ({ present: inject(RdxTabsPanel).present });
159
208
  /**
@@ -168,6 +217,7 @@ const panelPresenceContext = () => ({ present: inject(RdxTabsPanel).present });
168
217
  class RdxTabsPanel {
169
218
  constructor() {
170
219
  this.elementRef = inject(ElementRef);
220
+ this.listItem = inject(RdxCompositeListItem, { self: true });
171
221
  this.rootContext = injectTabsRootContext();
172
222
  /**
173
223
  * A unique value that associates the panel with a tab.
@@ -186,7 +236,15 @@ class RdxTabsPanel {
186
236
  /** @ignore */
187
237
  this.panelId = computed(() => makePanelId(this.rootContext.baseId, this.value()), ...(ngDevMode ? [{ debugName: "panelId" }] : /* istanbul ignore next */ []));
188
238
  /** @ignore */
189
- 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 */ []));
190
248
  /** Whether this panel's tab is currently selected. */
191
249
  this.active = computed(() => this.rootContext.value() === this.value(), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
192
250
  /** `true` once a `*rdxTabsPanelPresence` child takes over mounting. */
@@ -203,20 +261,18 @@ class RdxTabsPanel {
203
261
  * element renders nothing), unless `keepMounted` keeps the inactive contents around.
204
262
  */
205
263
  this.hidden = computed(() => !this.active() && this.transitionStatus() !== 'ending' && (!this.hasPresence() || this.keepMounted()), ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
206
- /** @ignore Index of the panel, derived from the order of its associated tab. */
207
- this.index = computed(() => {
208
- const list = this.rootContext.tabListElement();
209
- if (!list) {
210
- return null;
211
- }
212
- const tabs = Array.from(list.querySelectorAll('[role="tab"]'));
213
- const position = tabs.findIndex((tab) => tab.id === makeTabId(this.rootContext.baseId, this.value()));
214
- return position === -1 ? null : position;
215
- }, ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
264
+ /** @ignore Index of the panel in DOM order. */
265
+ this.index = this.listItem.index;
216
266
  this.previousActive = false;
217
267
  this.isFirstRun = true;
218
268
  const unregister = this.transition.registerElement(this.elementRef.nativeElement);
219
269
  inject(DestroyRef).onDestroy(unregister);
270
+ effect(() => {
271
+ this.listItem.setMetadata({
272
+ id: this.panelId(),
273
+ value: this.value()
274
+ });
275
+ });
220
276
  effect(() => {
221
277
  const active = this.active();
222
278
  // Settle the initial state without playing an enter transition.
@@ -236,7 +292,7 @@ class RdxTabsPanel {
236
292
  this.hasPresence.set(true);
237
293
  }
238
294
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsPanel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
239
- 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: { "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 }); }
240
296
  }
241
297
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsPanel, decorators: [{
242
298
  type: Directive,
@@ -244,10 +300,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
244
300
  selector: '[rdxTabsPanel]',
245
301
  exportAs: 'rdxTabsPanel',
246
302
  providers: [provideRdxPresenceContext(panelPresenceContext)],
303
+ hostDirectives: [RdxCompositeListItem],
247
304
  host: {
248
305
  role: 'tabpanel',
249
- '[id]': 'panelId()',
250
- '[attr.tabindex]': 'active() ? 0 : undefined',
306
+ '[attr.id]': 'panelId()',
307
+ '[attr.tabindex]': 'active() ? 0 : -1',
251
308
  '[attr.aria-labelledby]': 'tabId()',
252
309
  '[attr.data-orientation]': 'rootContext.orientation()',
253
310
  '[attr.data-activation-direction]': 'rootContext.activationDirection()',
@@ -292,9 +349,11 @@ const rootContext = () => {
292
349
  activationDirection: root.activationDirection.asReadonly(),
293
350
  activateOnFocus: root.activateOnFocus.asReadonly(),
294
351
  tabListElement: root.tabListElement.asReadonly(),
352
+ tabMap: root.tabMap.asReadonly(),
295
353
  setValue: (value, event, reason) => root.setValue(value, event, reason),
296
354
  setActivateOnFocus: (value) => root.activateOnFocus.set(value),
297
- setTabListElement: (element) => root.tabListElement.set(element)
355
+ setTabListElement: (element) => root.tabListElement.set(element),
356
+ setTabMap: (map) => root.tabMap.set(map)
298
357
  };
299
358
  };
300
359
  /**
@@ -312,8 +371,11 @@ class RdxTabsRoot {
312
371
  this.value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
313
372
  /**
314
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
315
377
  */
316
- this.defaultValue = input(...(ngDevMode ? [undefined, { debugName: "defaultValue" }] : /* istanbul ignore next */ []));
378
+ this.defaultValue = input(undefined, ...(ngDevMode ? [{ debugName: "defaultValue" }] : /* istanbul ignore next */ []));
317
379
  /**
318
380
  * The orientation the tabs are laid out. Controls arrow-key navigation
319
381
  * (left/right vs. up/down).
@@ -323,55 +385,146 @@ class RdxTabsRoot {
323
385
  this.orientation = input('horizontal', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
324
386
  /**
325
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.
326
392
  */
327
393
  this.onValueChange = output();
328
394
  /** @ignore Set by `[rdxTabsList]`. */
329
395
  this.activateOnFocus = signal(false, ...(ngDevMode ? [{ debugName: "activateOnFocus" }] : /* istanbul ignore next */ []));
330
396
  /** @ignore Set by `[rdxTabsList]`. */
331
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 */ []));
332
400
  /** @ignore */
333
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;
409
+ effect(() => {
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
+ });
334
430
  effect(() => {
335
- const initial = this.defaultValue();
336
- if (initial !== undefined && untracked(this.value) === undefined) {
337
- this.value.set(initial);
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;
338
486
  }
339
487
  });
340
488
  }
341
489
  /** @ignore */
342
- setValue(value, event, reason = event ? 'trigger-press' : 'none') {
490
+ setValue(value, event, reason = 'none') {
343
491
  const previous = this.value();
344
492
  if (previous === value) {
345
493
  return;
346
494
  }
347
495
  const trigger = event?.currentTarget instanceof HTMLElement ? event.currentTarget : undefined;
348
- 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;
349
500
  this.onValueChange.emit({ value, eventDetails });
350
501
  if (eventDetails.isCanceled()) {
351
502
  return;
352
503
  }
353
- this.activationDirection.set(this.computeDirection(previous, value));
504
+ this.activationDirection.set(activationDirection);
505
+ this.commitValue(value);
506
+ }
507
+ commitValue(value) {
508
+ this.internalValueCommit = true;
354
509
  this.value.set(value);
355
510
  }
356
- computeDirection(previous, next) {
357
- const list = this.tabListElement();
358
- if (!list || previous === undefined || previous === null) {
359
- return 'none';
360
- }
361
- const tabs = Array.from(list.querySelectorAll('[role="tab"]'));
362
- const previousIndex = tabs.findIndex((tab) => tab.id === makeTabId(this.baseId, previous));
363
- const nextIndex = tabs.findIndex((tab) => tab.id === makeTabId(this.baseId, next));
364
- if (previousIndex === -1 || nextIndex === -1 || previousIndex === nextIndex) {
365
- return 'none';
366
- }
367
- const horizontal = this.orientation() === 'horizontal';
368
- if (nextIndex > previousIndex) {
369
- 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;
370
520
  }
371
- 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 });
372
525
  }
373
526
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
374
- 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 }); }
375
528
  }
376
529
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsRoot, decorators: [{
377
530
  type: Directive,
@@ -379,12 +532,87 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
379
532
  selector: '[rdxTabsRoot]',
380
533
  exportAs: 'rdxTabsRoot',
381
534
  providers: [provideTabsRootContext(rootContext)],
535
+ hostDirectives: [RdxCompositeList],
382
536
  host: {
383
537
  '[attr.data-orientation]': 'orientation()',
384
538
  '[attr.data-activation-direction]': 'activationDirection()'
385
539
  }
386
540
  }]
387
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
+ }
388
616
 
389
617
  /**
390
618
  * An individual interactive tab button that activates its corresponding panel.
@@ -394,80 +622,114 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
394
622
  class RdxTabsTab {
395
623
  constructor() {
396
624
  this.rootContext = injectTabsRootContext();
397
- this.rovingFocusItem = inject(RdxRovingFocusItemDirective);
625
+ this.compositeItem = inject(RdxCompositeItem, { self: true });
398
626
  /**
399
627
  * A unique value that associates the tab with a panel.
400
628
  */
401
629
  this.value = input.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
402
630
  /**
403
631
  * When `true`, prevents the user from interacting with the tab.
632
+ * Disabled tabs remain focusable during composite keyboard navigation, matching Base UI.
404
633
  */
405
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 */ []));
406
645
  /** @ignore */
407
- 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 */ []));
408
647
  /** @ignore */
409
648
  this.panelId = computed(() => makePanelId(this.rootContext.baseId, this.value()), ...(ngDevMode ? [{ debugName: "panelId" }] : /* istanbul ignore next */ []));
410
649
  /** @ignore */
411
650
  this.active = computed(() => this.rootContext.value() === this.value(), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
651
+ this.isPressing = false;
652
+ this.isMainButton = false;
412
653
  effect(() => {
413
- this.rovingFocusItem.setActive(this.active());
414
- this.rovingFocusItem.setFocusable(!this.disabled());
654
+ this.compositeItem.setMetadata({
655
+ disabled: this.disabled(),
656
+ id: this.tabId(),
657
+ value: this.value()
658
+ });
415
659
  });
416
660
  }
417
661
  /** @ignore */
418
- onMouseDown(event) {
419
- // Only the primary button selects; ignore Ctrl-click (macOS right-click emulation).
420
- if (!this.disabled() && event.button === 0 && !event.ctrlKey) {
421
- this.rootContext.setValue(this.value(), event, 'trigger-press');
422
- }
423
- else {
424
- // Prevent focus to avoid accidental activation.
425
- event.preventDefault();
662
+ onClick(event) {
663
+ if (this.active() || this.disabled()) {
664
+ return;
426
665
  }
666
+ this.rootContext.setValue(this.value(), event, 'none');
427
667
  }
428
668
  /** @ignore */
429
669
  onKeyDown(event) {
430
670
  if (!this.disabled() && (event.key === ' ' || event.key === 'Enter')) {
431
- this.rootContext.setValue(this.value(), event, 'keyboard');
671
+ this.rootContext.setValue(this.value(), event, 'none');
672
+ }
673
+ }
674
+ /** @ignore */
675
+ onPointerDown(event) {
676
+ if (this.active() || this.disabled()) {
677
+ return;
432
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 });
433
694
  }
434
695
  /** @ignore */
435
696
  onFocus(event) {
436
- if (!this.active() && !this.disabled() && this.rootContext.activateOnFocus()) {
437
- 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');
438
702
  }
439
703
  }
440
704
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsTab, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
441
- 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: { "id": "tabId()", "attr.aria-selected": "active()", "attr.aria-controls": "panelId()", "attr.data-orientation": "rootContext.orientation()", "attr.data-activation-direction": "rootContext.activationDirection()", "attr.data-active": "active() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined", "attr.disabled": "disabled() ? \"\" : undefined" } }, exportAs: ["rdxTabsTab"], hostDirectives: [{ directive: i1.RdxRovingFocusItemDirective, inputs: ["allowShiftKey", "allowShiftKey"] }], 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 }); }
442
706
  }
443
707
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxTabsTab, decorators: [{
444
708
  type: Directive,
445
709
  args: [{
446
710
  selector: '[rdxTabsTab]',
447
711
  exportAs: 'rdxTabsTab',
448
- hostDirectives: [
449
- {
450
- directive: RdxRovingFocusItemDirective,
451
- inputs: ['allowShiftKey']
452
- }
453
- ],
712
+ hostDirectives: [RdxCompositeItem],
454
713
  host: {
455
- type: 'button',
714
+ '[attr.type]': 'nativeButton() ? "button" : undefined',
456
715
  role: 'tab',
457
- '[id]': 'tabId()',
716
+ '[attr.id]': 'tabId()',
458
717
  '[attr.aria-selected]': 'active()',
459
718
  '[attr.aria-controls]': 'panelId()',
719
+ '[attr.aria-disabled]': 'disabled() ? "true" : undefined',
720
+ '[attr.disabled]': 'null',
721
+ '[attr.data-composite-item-active]': 'active() ? "" : undefined',
460
722
  '[attr.data-orientation]': 'rootContext.orientation()',
461
723
  '[attr.data-activation-direction]': 'rootContext.activationDirection()',
462
724
  '[attr.data-active]': 'active() ? "" : undefined',
463
725
  '[attr.data-disabled]': 'disabled() ? "" : undefined',
464
- '[attr.disabled]': 'disabled() ? "" : undefined',
465
- '(mousedown)': 'onMouseDown($event)',
726
+ '(click)': 'onClick($event)',
466
727
  '(keydown)': 'onKeyDown($event)',
728
+ '(pointerdown)': 'onPointerDown($event)',
467
729
  '(focus)': 'onFocus($event)'
468
730
  }
469
731
  }]
470
- }], 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 }] }] } });
471
733
 
472
734
  const tabsImports = [RdxTabsRoot, RdxTabsList, RdxTabsTab, RdxTabsPanel, RdxTabsPanelPresence, RdxTabsIndicator];
473
735
  class RdxTabsModule {