@dashforge/tw 0.10.0-beta → 0.11.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/index.esm.js +133 -24
- package/dist/src/components/Tabs/Tabs.d.ts +8 -4
- package/dist/src/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/src/components/Tabs/tabs.types.d.ts +8 -2
- package/dist/src/components/Tabs/tabs.types.d.ts.map +1 -1
- package/dist/src/components/Tabs/useTabs.d.ts +55 -0
- package/dist/src/components/Tabs/useTabs.d.ts.map +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/components/Tabs/Tabs.test.tsx +85 -14
- package/src/components/Tabs/Tabs.tsx +35 -24
- package/src/components/Tabs/tabs.types.ts +8 -2
- package/src/components/Tabs/useTabs.ts +167 -0
- package/src/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,28 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
12
12
|
> duplicated intentionally — no shared "lowest common denominator" headless
|
|
13
13
|
> layer.
|
|
14
14
|
|
|
15
|
+
## [0.11.0-beta] — 2026-05-22
|
|
16
|
+
|
|
17
|
+
**Sprint 7 — Tabs.** `<Tabs>` rebuilt as a custom, clean-room component.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **`<Tabs>` rebuilt without Radix.** The component is reimplemented on a
|
|
22
|
+
headless `useTabs` engine (the WAI-ARIA APG keyboard model) — the
|
|
23
|
+
`@radix-ui/react-tabs` dependency is removed. The public API
|
|
24
|
+
(`TabsProps` / `TabItem` / `TabsSlotProps` / `tabsVariants`) is
|
|
25
|
+
**unchanged** — an internal swap, not a breaking change for consumers.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`keepMounted`** prop on `<Tabs>` — keeps inactive panels mounted
|
|
30
|
+
(hidden) instead of unmounting them. Default `false`.
|
|
31
|
+
|
|
32
|
+
### Removed
|
|
33
|
+
|
|
34
|
+
- Runtime dependency `@radix-ui/react-tabs` — the custom `useTabs` engine
|
|
35
|
+
replaces it.
|
|
36
|
+
|
|
15
37
|
## [0.10.0-beta] — 2026-05-21
|
|
16
38
|
|
|
17
39
|
**Sprint 7 — Calendar suite (part 2).** Completes the Tailwind date-picker
|
package/dist/index.esm.js
CHANGED
|
@@ -14,7 +14,6 @@ import * as RadixRadioGroup from '@radix-ui/react-radio-group';
|
|
|
14
14
|
import { useCalendar, resolveCalendarKey, formatLongDate, parseISODate, formatTime, generateTimeOptions, timeStringToMinutes, getTodayISODate, useDateRange } from '@dashforge/calendar-core';
|
|
15
15
|
import * as RadixPopover from '@radix-ui/react-popover';
|
|
16
16
|
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
17
|
-
import * as RadixTabs from '@radix-ui/react-tabs';
|
|
18
17
|
import * as RadixTooltip from '@radix-ui/react-tooltip';
|
|
19
18
|
import * as RadixAccordion from '@radix-ui/react-accordion';
|
|
20
19
|
|
|
@@ -8486,51 +8485,161 @@ const tabsVariants = tv({
|
|
|
8486
8485
|
}
|
|
8487
8486
|
});
|
|
8488
8487
|
|
|
8488
|
+
function useTabs(options) {
|
|
8489
|
+
var _ref;
|
|
8490
|
+
var _items_;
|
|
8491
|
+
const { items, value, defaultValue, onValueChange, orientation } = options;
|
|
8492
|
+
const baseId = useId();
|
|
8493
|
+
const firstValue = (_ref = (_items_ = items[0]) == null ? void 0 : _items_.value) != null ? _ref : '';
|
|
8494
|
+
const [internalValue, setInternalValue] = useState(defaultValue != null ? defaultValue : firstValue);
|
|
8495
|
+
const isControlled = value !== undefined;
|
|
8496
|
+
const activeValue = isControlled ? value : internalValue;
|
|
8497
|
+
// Tab buttons, keyed by value — used to move DOM focus on keyboard nav.
|
|
8498
|
+
const tabRefs = useRef(new Map());
|
|
8499
|
+
const select = useCallback((next)=>{
|
|
8500
|
+
if (!isControlled) {
|
|
8501
|
+
setInternalValue(next);
|
|
8502
|
+
}
|
|
8503
|
+
onValueChange == null ? void 0 : onValueChange(next);
|
|
8504
|
+
}, [
|
|
8505
|
+
isControlled,
|
|
8506
|
+
onValueChange
|
|
8507
|
+
]);
|
|
8508
|
+
const handleKeyDown = useCallback((event)=>{
|
|
8509
|
+
var _tabRefs_current_get;
|
|
8510
|
+
const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
|
|
8511
|
+
const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
|
|
8512
|
+
const enabled = items.filter((item)=>item.disabled !== true);
|
|
8513
|
+
if (enabled.length === 0) {
|
|
8514
|
+
return;
|
|
8515
|
+
}
|
|
8516
|
+
const currentIndex = enabled.findIndex((item)=>item.value === activeValue);
|
|
8517
|
+
let targetIndex = null;
|
|
8518
|
+
if (event.key === nextKey) {
|
|
8519
|
+
targetIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % enabled.length;
|
|
8520
|
+
} else if (event.key === prevKey) {
|
|
8521
|
+
targetIndex = currentIndex < 0 ? enabled.length - 1 : (currentIndex - 1 + enabled.length) % enabled.length;
|
|
8522
|
+
} else if (event.key === 'Home') {
|
|
8523
|
+
targetIndex = 0;
|
|
8524
|
+
} else if (event.key === 'End') {
|
|
8525
|
+
targetIndex = enabled.length - 1;
|
|
8526
|
+
}
|
|
8527
|
+
if (targetIndex === null) {
|
|
8528
|
+
return;
|
|
8529
|
+
}
|
|
8530
|
+
event.preventDefault();
|
|
8531
|
+
const target = enabled[targetIndex];
|
|
8532
|
+
if (target === undefined) {
|
|
8533
|
+
return;
|
|
8534
|
+
}
|
|
8535
|
+
select(target.value);
|
|
8536
|
+
(_tabRefs_current_get = tabRefs.current.get(target.value)) == null ? void 0 : _tabRefs_current_get.focus();
|
|
8537
|
+
}, [
|
|
8538
|
+
items,
|
|
8539
|
+
orientation,
|
|
8540
|
+
activeValue,
|
|
8541
|
+
select
|
|
8542
|
+
]);
|
|
8543
|
+
const getTabListProps = ()=>({
|
|
8544
|
+
role: 'tablist',
|
|
8545
|
+
'aria-orientation': orientation,
|
|
8546
|
+
onKeyDown: handleKeyDown
|
|
8547
|
+
});
|
|
8548
|
+
const getTabProps = (item)=>{
|
|
8549
|
+
const active = item.value === activeValue;
|
|
8550
|
+
return {
|
|
8551
|
+
role: 'tab',
|
|
8552
|
+
id: `${baseId}-tab-${item.value}`,
|
|
8553
|
+
'aria-selected': active,
|
|
8554
|
+
'aria-controls': `${baseId}-panel-${item.value}`,
|
|
8555
|
+
tabIndex: active ? 0 : -1,
|
|
8556
|
+
disabled: item.disabled,
|
|
8557
|
+
'data-state': active ? 'active' : 'inactive',
|
|
8558
|
+
onClick: ()=>{
|
|
8559
|
+
if (item.disabled !== true) {
|
|
8560
|
+
select(item.value);
|
|
8561
|
+
}
|
|
8562
|
+
},
|
|
8563
|
+
ref: (el)=>{
|
|
8564
|
+
if (el) {
|
|
8565
|
+
tabRefs.current.set(item.value, el);
|
|
8566
|
+
} else {
|
|
8567
|
+
tabRefs.current.delete(item.value);
|
|
8568
|
+
}
|
|
8569
|
+
}
|
|
8570
|
+
};
|
|
8571
|
+
};
|
|
8572
|
+
const getPanelProps = (item)=>{
|
|
8573
|
+
const active = item.value === activeValue;
|
|
8574
|
+
return {
|
|
8575
|
+
role: 'tabpanel',
|
|
8576
|
+
id: `${baseId}-panel-${item.value}`,
|
|
8577
|
+
'aria-labelledby': `${baseId}-tab-${item.value}`,
|
|
8578
|
+
tabIndex: 0,
|
|
8579
|
+
hidden: !active,
|
|
8580
|
+
'data-state': active ? 'active' : 'inactive'
|
|
8581
|
+
};
|
|
8582
|
+
};
|
|
8583
|
+
return {
|
|
8584
|
+
activeValue,
|
|
8585
|
+
getTabListProps,
|
|
8586
|
+
getTabProps,
|
|
8587
|
+
getPanelProps
|
|
8588
|
+
};
|
|
8589
|
+
}
|
|
8590
|
+
|
|
8489
8591
|
/**
|
|
8490
8592
|
* Dashforge TW `<Tabs>` — declarative tab navigation.
|
|
8491
8593
|
*
|
|
8492
|
-
*
|
|
8493
|
-
*
|
|
8494
|
-
*
|
|
8495
|
-
*
|
|
8594
|
+
* Custom clean-room implementation on the headless `useTabs` engine — no
|
|
8595
|
+
* runtime UI dependency. Implements the WAI-ARIA APG tabs pattern: arrow-key
|
|
8596
|
+
* navigation, automatic activation, roving tabindex, `role="tablist"` +
|
|
8597
|
+
* `role="tab"` + `role="tabpanel"`.
|
|
8496
8598
|
*
|
|
8497
8599
|
* Two variant axes:
|
|
8498
8600
|
* - `variant` — `underline` (default) | `pill`
|
|
8499
8601
|
* - `orientation` — `horizontal` (default) | `vertical`
|
|
8602
|
+
*
|
|
8603
|
+
* Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
8604
|
+
* `keepMounted` keeps inactive panels in the DOM (default: only the active
|
|
8605
|
+
* panel is mounted).
|
|
8500
8606
|
*/ function Tabs(props) {
|
|
8501
|
-
var
|
|
8502
|
-
const { items, value, defaultValue, onValueChange, variant = 'underline', orientation = 'horizontal', sx, slotProps } = props;
|
|
8607
|
+
var _slotProps_root, _slotProps_list;
|
|
8608
|
+
const { items, value, defaultValue, onValueChange, variant = 'underline', orientation = 'horizontal', keepMounted = false, sx, slotProps } = props;
|
|
8503
8609
|
const v = tabsVariants({
|
|
8504
8610
|
variant,
|
|
8505
8611
|
orientation
|
|
8506
8612
|
});
|
|
8507
|
-
const
|
|
8508
|
-
|
|
8509
|
-
value
|
|
8510
|
-
defaultValue
|
|
8511
|
-
onValueChange
|
|
8512
|
-
orientation
|
|
8613
|
+
const tabs = useTabs({
|
|
8614
|
+
items,
|
|
8615
|
+
value,
|
|
8616
|
+
defaultValue,
|
|
8617
|
+
onValueChange,
|
|
8618
|
+
orientation
|
|
8619
|
+
});
|
|
8620
|
+
// Either all panels (hidden when inactive) or just the active one.
|
|
8621
|
+
const panels = keepMounted ? items : items.filter((item)=>item.value === tabs.activeValue);
|
|
8622
|
+
return jsxs("div", {
|
|
8513
8623
|
className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
|
|
8514
8624
|
children: [
|
|
8515
|
-
jsx(
|
|
8625
|
+
jsx("div", _extends({}, tabs.getTabListProps(), {
|
|
8516
8626
|
className: cn(v.list(), slotProps == null ? void 0 : (_slotProps_list = slotProps.list) == null ? void 0 : _slotProps_list.className),
|
|
8517
8627
|
children: items.map((item)=>{
|
|
8518
8628
|
var _slotProps_trigger;
|
|
8519
|
-
return jsx(
|
|
8520
|
-
|
|
8521
|
-
|
|
8629
|
+
return jsx("button", _extends({
|
|
8630
|
+
type: "button"
|
|
8631
|
+
}, tabs.getTabProps(item), {
|
|
8522
8632
|
className: cn(v.trigger(), slotProps == null ? void 0 : (_slotProps_trigger = slotProps.trigger) == null ? void 0 : _slotProps_trigger.className),
|
|
8523
8633
|
children: item.label
|
|
8524
|
-
}, item.value);
|
|
8634
|
+
}), item.value);
|
|
8525
8635
|
})
|
|
8526
|
-
}),
|
|
8527
|
-
|
|
8636
|
+
})),
|
|
8637
|
+
panels.map((item)=>{
|
|
8528
8638
|
var _slotProps_content;
|
|
8529
|
-
return jsx(
|
|
8530
|
-
value: item.value,
|
|
8639
|
+
return jsx("div", _extends({}, tabs.getPanelProps(item), {
|
|
8531
8640
|
className: cn(v.content(), slotProps == null ? void 0 : (_slotProps_content = slotProps.content) == null ? void 0 : _slotProps_content.className),
|
|
8532
8641
|
children: item.content
|
|
8533
|
-
}, item.value);
|
|
8642
|
+
}), item.value);
|
|
8534
8643
|
})
|
|
8535
8644
|
]
|
|
8536
8645
|
});
|
|
@@ -12795,6 +12904,6 @@ function SearchIcon(props) {
|
|
|
12795
12904
|
*/ // ───── Components ─────
|
|
12796
12905
|
/**
|
|
12797
12906
|
* Package version (synced with `package.json` at publish time).
|
|
12798
|
-
*/ const VERSION = '0.
|
|
12907
|
+
*/ const VERSION = '0.10.0-beta';
|
|
12799
12908
|
|
|
12800
12909
|
export { Accordion, AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Calendar, Checkbox, ConfirmDialogProvider, Container, DataGrid, DatePicker, DateRangePicker, DateTimePicker, Dialog, Divider, Grid, LeftNav, NumberField, OTPField, Pagination, Popover, RadioGroup, RenderButton, RenderChip, RenderText, RenderTwoLine, RowActionsMenu, Skeleton, SnackbarProvider, Stack, Switch, Table, Tabs, TextField, Textarea, TimePicker, Tooltip, TopBar, Typography, VERSION, VisuallyHidden, accordionVariants, appShellVariants, autocompleteVariants, boxVariants, breadcrumbsVariants, buttonVariants, calendarDayVariants, calendarVariants, checkboxVariants, cn, confirmDialogVariants, containerVariants, dataGridVariants, datePickerVariants, dateRangeDayVariants, dateRangePickerVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, getNestedValue, gridVariants, leftNavVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, radioGroupVariants, skeletonVariants, snackbarVariants, stackVariants, switchVariants, tableVariants, tabsVariants, textFieldVariants, textareaVariants, timePickerVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };
|
|
@@ -2,14 +2,18 @@ import type { TabsProps } from './tabs.types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Dashforge TW `<Tabs>` — declarative tab navigation.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Custom clean-room implementation on the headless `useTabs` engine — no
|
|
6
|
+
* runtime UI dependency. Implements the WAI-ARIA APG tabs pattern: arrow-key
|
|
7
|
+
* navigation, automatic activation, roving tabindex, `role="tablist"` +
|
|
8
|
+
* `role="tab"` + `role="tabpanel"`.
|
|
9
9
|
*
|
|
10
10
|
* Two variant axes:
|
|
11
11
|
* - `variant` — `underline` (default) | `pill`
|
|
12
12
|
* - `orientation` — `horizontal` (default) | `vertical`
|
|
13
|
+
*
|
|
14
|
+
* Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
15
|
+
* `keepMounted` keeps inactive panels in the DOM (default: only the active
|
|
16
|
+
* panel is mounted).
|
|
13
17
|
*/
|
|
14
18
|
export declare function Tabs(props: TabsProps): import("react/jsx-runtime").JSX.Element;
|
|
15
19
|
//# sourceMappingURL=Tabs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD
|
|
1
|
+
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,2CAuDpC"}
|
|
@@ -31,7 +31,8 @@ export interface TabItem {
|
|
|
31
31
|
/**
|
|
32
32
|
* Props for `<Tabs>`.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
34
|
+
* Custom clean-room implementation — the headless `useTabs` engine, no Radix.
|
|
35
|
+
* Two variant axes:
|
|
35
36
|
* - `variant` — `underline` (default) | `pill`
|
|
36
37
|
* - `orientation` — `horizontal` (default) | `vertical`
|
|
37
38
|
*
|
|
@@ -39,7 +40,7 @@ export interface TabItem {
|
|
|
39
40
|
* (`defaultValue`) modes — the `value` prop wins when both are set.
|
|
40
41
|
*/
|
|
41
42
|
export interface TabsProps extends TabsVariants {
|
|
42
|
-
/** Tab items array
|
|
43
|
+
/** Tab items array. */
|
|
43
44
|
items: TabItem[];
|
|
44
45
|
/** Controlled active value. */
|
|
45
46
|
value?: string;
|
|
@@ -47,6 +48,11 @@ export interface TabsProps extends TabsVariants {
|
|
|
47
48
|
defaultValue?: string;
|
|
48
49
|
/** Fires when the active tab changes. */
|
|
49
50
|
onValueChange?: (value: string) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Keep inactive panels mounted (hidden via the `hidden` attribute).
|
|
53
|
+
* Default `false` — only the active panel is in the DOM.
|
|
54
|
+
*/
|
|
55
|
+
keepMounted?: boolean;
|
|
50
56
|
/** Root className shortcut. */
|
|
51
57
|
sx?: string;
|
|
52
58
|
/** Per-slot overrides. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.types.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/tabs.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAClC;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB,yBAAyB;IACzB,OAAO,EAAE,SAAS,CAAC;IACnB,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED
|
|
1
|
+
{"version":3,"file":"tabs.types.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/tabs.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAClC;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB,yBAAyB;IACzB,OAAO,EAAE,SAAS,CAAC;IACnB,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,SAAU,SAAQ,YAAY;IAC7C,uBAAuB;IACvB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+BAA+B;IAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { KeyboardEvent } from 'react';
|
|
2
|
+
import type { TabItem } from './tabs.types.js';
|
|
3
|
+
/**
|
|
4
|
+
* `useTabs` — the headless tab engine for the Tailwind `<Tabs>`.
|
|
5
|
+
*
|
|
6
|
+
* Pure logic, zero UI: active-value state (controlled / uncontrolled) plus
|
|
7
|
+
* the WAI-ARIA APG keyboard model (arrow keys, Home/End, roving tabindex,
|
|
8
|
+
* automatic activation, disabled-skip, edge wrap). Returns attribute bundles
|
|
9
|
+
* for the `tablist` / `tab` / `tabpanel` roles.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately NOT a shared package: the tab logic is ~90 lines — too thin
|
|
12
|
+
* to justify a published `tabs-core`. The MUI skin keeps its own near-identical
|
|
13
|
+
* copy. Only the public API contract is shared.
|
|
14
|
+
*/
|
|
15
|
+
interface UseTabsOptions {
|
|
16
|
+
items: TabItem[];
|
|
17
|
+
value: string | undefined;
|
|
18
|
+
defaultValue: string | undefined;
|
|
19
|
+
onValueChange: ((value: string) => void) | undefined;
|
|
20
|
+
orientation: 'horizontal' | 'vertical';
|
|
21
|
+
}
|
|
22
|
+
interface TabListAttributes {
|
|
23
|
+
role: 'tablist';
|
|
24
|
+
'aria-orientation': 'horizontal' | 'vertical';
|
|
25
|
+
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
26
|
+
}
|
|
27
|
+
interface TabAttributes {
|
|
28
|
+
role: 'tab';
|
|
29
|
+
id: string;
|
|
30
|
+
'aria-selected': boolean;
|
|
31
|
+
'aria-controls': string;
|
|
32
|
+
tabIndex: number;
|
|
33
|
+
disabled: boolean | undefined;
|
|
34
|
+
'data-state': 'active' | 'inactive';
|
|
35
|
+
onClick: () => void;
|
|
36
|
+
ref: (el: HTMLButtonElement | null) => void;
|
|
37
|
+
}
|
|
38
|
+
interface PanelAttributes {
|
|
39
|
+
role: 'tabpanel';
|
|
40
|
+
id: string;
|
|
41
|
+
'aria-labelledby': string;
|
|
42
|
+
tabIndex: number;
|
|
43
|
+
hidden: boolean;
|
|
44
|
+
'data-state': 'active' | 'inactive';
|
|
45
|
+
}
|
|
46
|
+
export interface UseTabsResult {
|
|
47
|
+
/** The currently active tab value. */
|
|
48
|
+
activeValue: string;
|
|
49
|
+
getTabListProps: () => TabListAttributes;
|
|
50
|
+
getTabProps: (item: TabItem) => TabAttributes;
|
|
51
|
+
getPanelProps: (item: TabItem) => PanelAttributes;
|
|
52
|
+
}
|
|
53
|
+
export declare function useTabs(options: UseTabsOptions): UseTabsResult;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=useTabs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTabs.d.ts","sourceRoot":"","sources":["../../../../src/components/Tabs/useTabs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C;;;;;;;;;;;GAWG;AACH,UAAU,cAAc;IACtB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IACrD,WAAW,EAAE,YAAY,GAAG,UAAU,CAAC;CACxC;AAED,UAAU,iBAAiB;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,kBAAkB,EAAE,YAAY,GAAG,UAAU,CAAC;IAC9C,SAAS,EAAE,CAAC,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;CAC3D;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,KAAK,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,YAAY,EAAE,QAAQ,GAAG,UAAU,CAAC;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,GAAG,EAAE,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI,KAAK,IAAI,CAAC;CAC7C;AAED,UAAU,eAAe;IACvB,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,QAAQ,GAAG,UAAU,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,aAAa,CAAC;IAC9C,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,eAAe,CAAC;CACnD;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,aAAa,CA2G9D"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -142,5 +142,5 @@ export type { VariantProps } from 'tailwind-variants';
|
|
|
142
142
|
/**
|
|
143
143
|
* Package version (synced with `package.json` at publish time).
|
|
144
144
|
*/
|
|
145
|
-
export declare const VERSION = "0.
|
|
145
|
+
export declare const VERSION = "0.10.0-beta";
|
|
146
146
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAExE,OAAO,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AAChE,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAEjF,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,iBAAiB,GAClB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAE9E,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AACvD,YAAY,EACV,WAAW,EACX,eAAe,GAChB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAIxE,OAAO,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AACnE,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gDAAgD,CAAC;AAEpF,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,iBAAiB,GAClB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAI9E,OAAO,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AACtE,YAAY,EACV,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,+CAA+C,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kDAAkD,CAAC;AAEvF,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAI9E,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AACzE,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oDAAoD,CAAC;AAM1F,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,iBAAiB,GAClB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AACnE,YAAY,EACV,eAAe,EACf,mBAAmB,GACpB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gDAAgD,CAAC;AAEpF,OAAO,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AACnE,YAAY,EACV,eAAe,EACf,mBAAmB,GACpB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gDAAgD,CAAC;AAEpF,OAAO,EAAE,cAAc,EAAE,MAAM,+CAA+C,CAAC;AAC/E,YAAY,EACV,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,qDAAqD,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wDAAwD,CAAC;AAEhG,OAAO,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AAClF,YAAY,EACV,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,uDAAuD,CAAC;AAC/D,OAAO,EACL,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,0DAA0D,CAAC;AAIlE,OAAO,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AACtE,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,+CAA+C,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kDAAkD,CAAC;AAEvF,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAC1D,YAAY,EACV,YAAY,EACZ,WAAW,EACX,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,0CAA0C,CAAC;AAE3E,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AACvD,YAAY,EACV,WAAW,EACX,eAAe,GAChB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAExE,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,iBAAiB,GAClB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAI9E,OAAO,EACL,qBAAqB,EACrB,UAAU,GACX,MAAM,6CAA6C,CAAC;AACrD,YAAY,EACV,0BAA0B,EAC1B,sBAAsB,EACtB,SAAS,EACT,cAAc,EACd,eAAe,GAChB,MAAM,mDAAmD,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sDAAsD,CAAC;AAE7F,OAAO,EACL,gBAAgB,EAChB,WAAW,GACZ,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,qBAAqB,EACrB,WAAW,EACX,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAO9E,OAAO,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,gDAAgD,CAAC;AAEpF,OAAO,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAC9C,YAAY,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAE/D,OAAO,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AACpD,YAAY,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAErE,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AACjD,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,aAAa,EACb,OAAO,EACP,eAAe,GAChB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAOlE,OAAO,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,2CAA2C,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAEjF,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EACL,eAAe,EACf,mBAAmB,GACpB,MAAM,0CAA0C,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AACtE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AAEtF,OAAO,EAAE,cAAc,EAAE,MAAM,+CAA+C,CAAC;AAC/E,YAAY,EAAE,mBAAmB,EAAE,MAAM,qDAAqD,CAAC;AAI/F,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AACvD,YAAY,EACV,WAAW,EACX,eAAe,GAChB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAExE,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AACjD,YAAY,EACV,SAAS,EACT,aAAa,EACb,OAAO,GACR,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAElE,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAC1D,YAAY,EACV,YAAY,EACZ,gBAAgB,GACjB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,0CAA0C,CAAC;AAE3E,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAC1D,YAAY,EACV,YAAY,EACZ,gBAAgB,GACjB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,0CAA0C,CAAC;AAE3E,OAAO,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AAChE,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,aAAa,GACd,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAIjF,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,iBAAiB,GAClB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAE9E,OAAO,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AACnE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gDAAgD,CAAC;AAMpF,OAAO,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AACpD,YAAY,EACV,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,cAAc,EACd,WAAW,GACZ,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAErE,OAAO,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,GACf,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,cAAc,EAAE,MAAM,6CAA6C,CAAC;AAQ7E,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAG9E,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAG3D,OAAO,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAInC,OAAO,EAAE,EAAE,EAAE,MAAM,mBAAmB,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;GAEG;AACH,eAAO,MAAM,OAAO,gBAAgB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashforge/tw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-beta",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.esm.js",
|
|
@@ -23,15 +23,14 @@
|
|
|
23
23
|
"@radix-ui/react-radio-group": "^1.3.8",
|
|
24
24
|
"@radix-ui/react-slot": "^1.1.0",
|
|
25
25
|
"@radix-ui/react-switch": "^1.1.0",
|
|
26
|
-
"@radix-ui/react-tabs": "^1.1.0",
|
|
27
26
|
"@radix-ui/react-tooltip": "^1.1.0",
|
|
28
27
|
"clsx": "^2.1.0",
|
|
29
28
|
"tailwind-merge": "^3.0.0",
|
|
30
29
|
"tailwind-variants": "^3.1.1",
|
|
31
|
-
"@dashforge/calendar-core": "0.2.0-beta",
|
|
32
30
|
"@dashforge/ui-core": "0.2.3-beta",
|
|
31
|
+
"@dashforge/forms": "0.2.3-beta",
|
|
33
32
|
"@dashforge/rbac": "0.2.3-beta",
|
|
34
|
-
"@dashforge/
|
|
33
|
+
"@dashforge/calendar-core": "0.2.0-beta"
|
|
35
34
|
},
|
|
36
35
|
"peerDependencies": {
|
|
37
36
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -19,7 +19,6 @@ describe('Tabs — rendering', () => {
|
|
|
19
19
|
expect(screen.queryByRole('tab', { name: 'Overview' })).not.toBeNull();
|
|
20
20
|
expect(screen.queryByRole('tab', { name: 'Details' })).not.toBeNull();
|
|
21
21
|
expect(screen.queryByRole('tab', { name: 'History' })).not.toBeNull();
|
|
22
|
-
// Only the active panel renders content (others get data-state="inactive").
|
|
23
22
|
expect(screen.queryByText('Overview body')).not.toBeNull();
|
|
24
23
|
});
|
|
25
24
|
|
|
@@ -30,7 +29,7 @@ describe('Tabs — rendering', () => {
|
|
|
30
29
|
|
|
31
30
|
it('switches active panel via controlled `value` rerender', () => {
|
|
32
31
|
const { rerender } = render(
|
|
33
|
-
<Tabs items={items} value="overview" onValueChange={() => {}}
|
|
32
|
+
<Tabs items={items} value="overview" onValueChange={() => {}} />,
|
|
34
33
|
);
|
|
35
34
|
expect(screen.queryByText('Overview body')).not.toBeNull();
|
|
36
35
|
rerender(<Tabs items={items} value="details" onValueChange={() => {}} />);
|
|
@@ -39,14 +38,20 @@ describe('Tabs — rendering', () => {
|
|
|
39
38
|
|
|
40
39
|
it('marks the active tab via aria-selected', () => {
|
|
41
40
|
render(<Tabs items={items} defaultValue="details" />);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
expect(
|
|
41
|
+
expect(
|
|
42
|
+
screen.getByRole('tab', { name: 'Details' }).getAttribute('aria-selected'),
|
|
43
|
+
).toBe('true');
|
|
44
|
+
expect(
|
|
45
|
+
screen.getByRole('tab', { name: 'Overview' }).getAttribute('aria-selected'),
|
|
46
|
+
).toBe('false');
|
|
46
47
|
});
|
|
47
|
-
});
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
it('selects a tab on click', () => {
|
|
50
|
+
render(<Tabs items={items} />);
|
|
51
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Details' }));
|
|
52
|
+
expect(screen.queryByText('Details body')).not.toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
50
55
|
|
|
51
56
|
describe('Tabs — variants', () => {
|
|
52
57
|
it.each(['underline', 'pill'] as const)('renders variant=%s', (variant) => {
|
|
@@ -56,19 +61,85 @@ describe('Tabs — variants', () => {
|
|
|
56
61
|
|
|
57
62
|
it.each(['horizontal', 'vertical'] as const)('renders orientation=%s', (o) => {
|
|
58
63
|
render(<Tabs items={items} orientation={o} />);
|
|
59
|
-
|
|
60
|
-
expect(list.getAttribute('aria-orientation')).toBe(o);
|
|
64
|
+
expect(screen.getByRole('tablist').getAttribute('aria-orientation')).toBe(o);
|
|
61
65
|
});
|
|
62
66
|
});
|
|
63
67
|
|
|
64
68
|
describe('Tabs — disabled items', () => {
|
|
65
69
|
it('marks disabled triggers as disabled', () => {
|
|
70
|
+
const disabledItems = [...items.slice(0, 2), { ...items[2]!, disabled: true }];
|
|
71
|
+
render(<Tabs items={disabledItems} />);
|
|
72
|
+
const trigger = screen.getByRole('tab', {
|
|
73
|
+
name: 'History',
|
|
74
|
+
}) as HTMLButtonElement;
|
|
75
|
+
expect(trigger.disabled).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('skips disabled tabs during keyboard navigation', () => {
|
|
66
79
|
const disabledItems = [
|
|
67
|
-
|
|
68
|
-
{ ...items[
|
|
80
|
+
items[0]!,
|
|
81
|
+
{ ...items[1]!, disabled: true },
|
|
82
|
+
items[2]!,
|
|
69
83
|
];
|
|
70
84
|
render(<Tabs items={disabledItems} />);
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
fireEvent.keyDown(screen.getByRole('tablist'), { key: 'ArrowRight' });
|
|
86
|
+
// Overview → (skip disabled Details) → History
|
|
87
|
+
expect(
|
|
88
|
+
screen.getByRole('tab', { name: 'History' }).getAttribute('aria-selected'),
|
|
89
|
+
).toBe('true');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('Tabs — keyboard', () => {
|
|
94
|
+
it('ArrowRight activates the next tab', () => {
|
|
95
|
+
render(<Tabs items={items} />);
|
|
96
|
+
fireEvent.keyDown(screen.getByRole('tablist'), { key: 'ArrowRight' });
|
|
97
|
+
expect(
|
|
98
|
+
screen.getByRole('tab', { name: 'Details' }).getAttribute('aria-selected'),
|
|
99
|
+
).toBe('true');
|
|
100
|
+
expect(screen.queryByText('Details body')).not.toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('ArrowRight wraps at the end', () => {
|
|
104
|
+
render(<Tabs items={items} defaultValue="history" />);
|
|
105
|
+
fireEvent.keyDown(screen.getByRole('tablist'), { key: 'ArrowRight' });
|
|
106
|
+
expect(
|
|
107
|
+
screen.getByRole('tab', { name: 'Overview' }).getAttribute('aria-selected'),
|
|
108
|
+
).toBe('true');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('Home / End jump to the first / last tab', () => {
|
|
112
|
+
render(<Tabs items={items} defaultValue="details" />);
|
|
113
|
+
const list = screen.getByRole('tablist');
|
|
114
|
+
fireEvent.keyDown(list, { key: 'End' });
|
|
115
|
+
expect(
|
|
116
|
+
screen.getByRole('tab', { name: 'History' }).getAttribute('aria-selected'),
|
|
117
|
+
).toBe('true');
|
|
118
|
+
fireEvent.keyDown(list, { key: 'Home' });
|
|
119
|
+
expect(
|
|
120
|
+
screen.getByRole('tab', { name: 'Overview' }).getAttribute('aria-selected'),
|
|
121
|
+
).toBe('true');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('ArrowDown drives a vertical tablist', () => {
|
|
125
|
+
render(<Tabs items={items} orientation="vertical" />);
|
|
126
|
+
fireEvent.keyDown(screen.getByRole('tablist'), { key: 'ArrowDown' });
|
|
127
|
+
expect(
|
|
128
|
+
screen.getByRole('tab', { name: 'Details' }).getAttribute('aria-selected'),
|
|
129
|
+
).toBe('true');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('Tabs — keepMounted', () => {
|
|
134
|
+
it('mounts only the active panel by default', () => {
|
|
135
|
+
render(<Tabs items={items} defaultValue="overview" />);
|
|
136
|
+
expect(screen.queryByText('Overview body')).not.toBeNull();
|
|
137
|
+
expect(screen.queryByText('History body')).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('keeps every panel mounted when keepMounted is set', () => {
|
|
141
|
+
render(<Tabs items={items} defaultValue="overview" keepMounted />);
|
|
142
|
+
expect(screen.queryByText('Overview body')).not.toBeNull();
|
|
143
|
+
expect(screen.queryByText('History body')).not.toBeNull();
|
|
73
144
|
});
|
|
74
145
|
});
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import * as RadixTabs from '@radix-ui/react-tabs';
|
|
2
1
|
import { cn } from '../../utils/cn.js';
|
|
3
2
|
import { tabsVariants } from './tabs.variants.js';
|
|
3
|
+
import { useTabs } from './useTabs.js';
|
|
4
4
|
import type { TabsProps } from './tabs.types.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Dashforge TW `<Tabs>` — declarative tab navigation.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Custom clean-room implementation on the headless `useTabs` engine — no
|
|
10
|
+
* runtime UI dependency. Implements the WAI-ARIA APG tabs pattern: arrow-key
|
|
11
|
+
* navigation, automatic activation, roving tabindex, `role="tablist"` +
|
|
12
|
+
* `role="tab"` + `role="tabpanel"`.
|
|
13
13
|
*
|
|
14
14
|
* Two variant axes:
|
|
15
15
|
* - `variant` — `underline` (default) | `pill`
|
|
16
16
|
* - `orientation` — `horizontal` (default) | `vertical`
|
|
17
|
+
*
|
|
18
|
+
* Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
19
|
+
* `keepMounted` keeps inactive panels in the DOM (default: only the active
|
|
20
|
+
* panel is mounted).
|
|
17
21
|
*/
|
|
18
22
|
export function Tabs(props: TabsProps) {
|
|
19
23
|
const {
|
|
@@ -23,44 +27,51 @@ export function Tabs(props: TabsProps) {
|
|
|
23
27
|
onValueChange,
|
|
24
28
|
variant = 'underline',
|
|
25
29
|
orientation = 'horizontal',
|
|
30
|
+
keepMounted = false,
|
|
26
31
|
sx,
|
|
27
32
|
slotProps,
|
|
28
33
|
} = props;
|
|
29
34
|
|
|
30
35
|
const v = tabsVariants({ variant, orientation });
|
|
31
|
-
const
|
|
36
|
+
const tabs = useTabs({
|
|
37
|
+
items,
|
|
38
|
+
value,
|
|
39
|
+
defaultValue,
|
|
40
|
+
onValueChange,
|
|
41
|
+
orientation,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Either all panels (hidden when inactive) or just the active one.
|
|
45
|
+
const panels = keepMounted
|
|
46
|
+
? items
|
|
47
|
+
: items.filter((item) => item.value === tabs.activeValue);
|
|
32
48
|
|
|
33
49
|
return (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
onValueChange={onValueChange}
|
|
38
|
-
orientation={orientation}
|
|
39
|
-
className={cn(v.root(), sx, slotProps?.root?.className)}
|
|
40
|
-
>
|
|
41
|
-
<RadixTabs.List
|
|
50
|
+
<div className={cn(v.root(), sx, slotProps?.root?.className)}>
|
|
51
|
+
<div
|
|
52
|
+
{...tabs.getTabListProps()}
|
|
42
53
|
className={cn(v.list(), slotProps?.list?.className)}
|
|
43
54
|
>
|
|
44
55
|
{items.map((item) => (
|
|
45
|
-
<
|
|
56
|
+
<button
|
|
46
57
|
key={item.value}
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
type="button"
|
|
59
|
+
{...tabs.getTabProps(item)}
|
|
49
60
|
className={cn(v.trigger(), slotProps?.trigger?.className)}
|
|
50
61
|
>
|
|
51
62
|
{item.label}
|
|
52
|
-
</
|
|
63
|
+
</button>
|
|
53
64
|
))}
|
|
54
|
-
</
|
|
55
|
-
{
|
|
56
|
-
<
|
|
65
|
+
</div>
|
|
66
|
+
{panels.map((item) => (
|
|
67
|
+
<div
|
|
57
68
|
key={item.value}
|
|
58
|
-
|
|
69
|
+
{...tabs.getPanelProps(item)}
|
|
59
70
|
className={cn(v.content(), slotProps?.content?.className)}
|
|
60
71
|
>
|
|
61
72
|
{item.content}
|
|
62
|
-
</
|
|
73
|
+
</div>
|
|
63
74
|
))}
|
|
64
|
-
</
|
|
75
|
+
</div>
|
|
65
76
|
);
|
|
66
77
|
}
|
|
@@ -26,7 +26,8 @@ export interface TabItem {
|
|
|
26
26
|
/**
|
|
27
27
|
* Props for `<Tabs>`.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* Custom clean-room implementation — the headless `useTabs` engine, no Radix.
|
|
30
|
+
* Two variant axes:
|
|
30
31
|
* - `variant` — `underline` (default) | `pill`
|
|
31
32
|
* - `orientation` — `horizontal` (default) | `vertical`
|
|
32
33
|
*
|
|
@@ -34,7 +35,7 @@ export interface TabItem {
|
|
|
34
35
|
* (`defaultValue`) modes — the `value` prop wins when both are set.
|
|
35
36
|
*/
|
|
36
37
|
export interface TabsProps extends TabsVariants {
|
|
37
|
-
/** Tab items array
|
|
38
|
+
/** Tab items array. */
|
|
38
39
|
items: TabItem[];
|
|
39
40
|
/** Controlled active value. */
|
|
40
41
|
value?: string;
|
|
@@ -42,6 +43,11 @@ export interface TabsProps extends TabsVariants {
|
|
|
42
43
|
defaultValue?: string;
|
|
43
44
|
/** Fires when the active tab changes. */
|
|
44
45
|
onValueChange?: (value: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Keep inactive panels mounted (hidden via the `hidden` attribute).
|
|
48
|
+
* Default `false` — only the active panel is in the DOM.
|
|
49
|
+
*/
|
|
50
|
+
keepMounted?: boolean;
|
|
45
51
|
/** Root className shortcut. */
|
|
46
52
|
sx?: string;
|
|
47
53
|
/** Per-slot overrides. */
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useCallback, useId, useRef, useState } from 'react';
|
|
2
|
+
import type { KeyboardEvent } from 'react';
|
|
3
|
+
import type { TabItem } from './tabs.types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `useTabs` — the headless tab engine for the Tailwind `<Tabs>`.
|
|
7
|
+
*
|
|
8
|
+
* Pure logic, zero UI: active-value state (controlled / uncontrolled) plus
|
|
9
|
+
* the WAI-ARIA APG keyboard model (arrow keys, Home/End, roving tabindex,
|
|
10
|
+
* automatic activation, disabled-skip, edge wrap). Returns attribute bundles
|
|
11
|
+
* for the `tablist` / `tab` / `tabpanel` roles.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT a shared package: the tab logic is ~90 lines — too thin
|
|
14
|
+
* to justify a published `tabs-core`. The MUI skin keeps its own near-identical
|
|
15
|
+
* copy. Only the public API contract is shared.
|
|
16
|
+
*/
|
|
17
|
+
interface UseTabsOptions {
|
|
18
|
+
items: TabItem[];
|
|
19
|
+
value: string | undefined;
|
|
20
|
+
defaultValue: string | undefined;
|
|
21
|
+
onValueChange: ((value: string) => void) | undefined;
|
|
22
|
+
orientation: 'horizontal' | 'vertical';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TabListAttributes {
|
|
26
|
+
role: 'tablist';
|
|
27
|
+
'aria-orientation': 'horizontal' | 'vertical';
|
|
28
|
+
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TabAttributes {
|
|
32
|
+
role: 'tab';
|
|
33
|
+
id: string;
|
|
34
|
+
'aria-selected': boolean;
|
|
35
|
+
'aria-controls': string;
|
|
36
|
+
tabIndex: number;
|
|
37
|
+
disabled: boolean | undefined;
|
|
38
|
+
'data-state': 'active' | 'inactive';
|
|
39
|
+
onClick: () => void;
|
|
40
|
+
ref: (el: HTMLButtonElement | null) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PanelAttributes {
|
|
44
|
+
role: 'tabpanel';
|
|
45
|
+
id: string;
|
|
46
|
+
'aria-labelledby': string;
|
|
47
|
+
tabIndex: number;
|
|
48
|
+
hidden: boolean;
|
|
49
|
+
'data-state': 'active' | 'inactive';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface UseTabsResult {
|
|
53
|
+
/** The currently active tab value. */
|
|
54
|
+
activeValue: string;
|
|
55
|
+
getTabListProps: () => TabListAttributes;
|
|
56
|
+
getTabProps: (item: TabItem) => TabAttributes;
|
|
57
|
+
getPanelProps: (item: TabItem) => PanelAttributes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useTabs(options: UseTabsOptions): UseTabsResult {
|
|
61
|
+
const { items, value, defaultValue, onValueChange, orientation } = options;
|
|
62
|
+
|
|
63
|
+
const baseId = useId();
|
|
64
|
+
const firstValue = items[0]?.value ?? '';
|
|
65
|
+
const [internalValue, setInternalValue] = useState<string>(
|
|
66
|
+
defaultValue ?? firstValue,
|
|
67
|
+
);
|
|
68
|
+
const isControlled = value !== undefined;
|
|
69
|
+
const activeValue = isControlled ? value : internalValue;
|
|
70
|
+
|
|
71
|
+
// Tab buttons, keyed by value — used to move DOM focus on keyboard nav.
|
|
72
|
+
const tabRefs = useRef(new Map<string, HTMLButtonElement>());
|
|
73
|
+
|
|
74
|
+
const select = useCallback(
|
|
75
|
+
(next: string) => {
|
|
76
|
+
if (!isControlled) {
|
|
77
|
+
setInternalValue(next);
|
|
78
|
+
}
|
|
79
|
+
onValueChange?.(next);
|
|
80
|
+
},
|
|
81
|
+
[isControlled, onValueChange],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const handleKeyDown = useCallback(
|
|
85
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
86
|
+
const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
|
|
87
|
+
const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
|
|
88
|
+
const enabled = items.filter((item) => item.disabled !== true);
|
|
89
|
+
if (enabled.length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const currentIndex = enabled.findIndex(
|
|
93
|
+
(item) => item.value === activeValue,
|
|
94
|
+
);
|
|
95
|
+
let targetIndex: number | null = null;
|
|
96
|
+
if (event.key === nextKey) {
|
|
97
|
+
targetIndex =
|
|
98
|
+
currentIndex < 0 ? 0 : (currentIndex + 1) % enabled.length;
|
|
99
|
+
} else if (event.key === prevKey) {
|
|
100
|
+
targetIndex =
|
|
101
|
+
currentIndex < 0
|
|
102
|
+
? enabled.length - 1
|
|
103
|
+
: (currentIndex - 1 + enabled.length) % enabled.length;
|
|
104
|
+
} else if (event.key === 'Home') {
|
|
105
|
+
targetIndex = 0;
|
|
106
|
+
} else if (event.key === 'End') {
|
|
107
|
+
targetIndex = enabled.length - 1;
|
|
108
|
+
}
|
|
109
|
+
if (targetIndex === null) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
const target = enabled[targetIndex];
|
|
114
|
+
if (target === undefined) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
select(target.value);
|
|
118
|
+
tabRefs.current.get(target.value)?.focus();
|
|
119
|
+
},
|
|
120
|
+
[items, orientation, activeValue, select],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const getTabListProps = (): TabListAttributes => ({
|
|
124
|
+
role: 'tablist',
|
|
125
|
+
'aria-orientation': orientation,
|
|
126
|
+
onKeyDown: handleKeyDown,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const getTabProps = (item: TabItem): TabAttributes => {
|
|
130
|
+
const active = item.value === activeValue;
|
|
131
|
+
return {
|
|
132
|
+
role: 'tab',
|
|
133
|
+
id: `${baseId}-tab-${item.value}`,
|
|
134
|
+
'aria-selected': active,
|
|
135
|
+
'aria-controls': `${baseId}-panel-${item.value}`,
|
|
136
|
+
tabIndex: active ? 0 : -1,
|
|
137
|
+
disabled: item.disabled,
|
|
138
|
+
'data-state': active ? 'active' : 'inactive',
|
|
139
|
+
onClick: () => {
|
|
140
|
+
if (item.disabled !== true) {
|
|
141
|
+
select(item.value);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
ref: (el: HTMLButtonElement | null) => {
|
|
145
|
+
if (el) {
|
|
146
|
+
tabRefs.current.set(item.value, el);
|
|
147
|
+
} else {
|
|
148
|
+
tabRefs.current.delete(item.value);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const getPanelProps = (item: TabItem): PanelAttributes => {
|
|
155
|
+
const active = item.value === activeValue;
|
|
156
|
+
return {
|
|
157
|
+
role: 'tabpanel',
|
|
158
|
+
id: `${baseId}-panel-${item.value}`,
|
|
159
|
+
'aria-labelledby': `${baseId}-tab-${item.value}`,
|
|
160
|
+
tabIndex: 0,
|
|
161
|
+
hidden: !active,
|
|
162
|
+
'data-state': active ? 'active' : 'inactive',
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return { activeValue, getTabListProps, getTabProps, getPanelProps };
|
|
167
|
+
}
|
package/src/index.ts
CHANGED