@basic-ui/core 0.0.29 → 0.0.33

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 (130) hide show
  1. package/build/cjs/index.js +90 -71
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/FocusLock/useFocusLock.js +21 -7
  4. package/build/esm/FocusLock/useFocusLock.js.map +1 -1
  5. package/build/esm/Menu/MenuList.js +7 -5
  6. package/build/esm/Menu/MenuList.js.map +1 -1
  7. package/build/esm/Tooltip/Tooltip.d.ts +1 -0
  8. package/build/esm/Tooltip/Tooltip.js +10 -3
  9. package/build/esm/Tooltip/Tooltip.js.map +1 -1
  10. package/build/esm/Tooltip/stateMachine.d.ts +17 -19
  11. package/build/esm/Tooltip/stateMachine.js +45 -49
  12. package/build/esm/Tooltip/stateMachine.js.map +1 -1
  13. package/build/esm/Tooltip/useTooltip.js +9 -9
  14. package/build/esm/Tooltip/useTooltip.js.map +1 -1
  15. package/build/tsconfig.tsbuildinfo +384 -89
  16. package/package.json +5 -5
  17. package/src/Accordion/Accordion.story.tsx +72 -0
  18. package/src/Accordion/Accordion.tsx +51 -0
  19. package/src/Accordion/AccordionBody.tsx +53 -0
  20. package/src/Accordion/AccordionHeader.tsx +165 -0
  21. package/src/Accordion/AccordionItem.tsx +43 -0
  22. package/src/Accordion/context.ts +35 -0
  23. package/src/Accordion/index.ts +4 -0
  24. package/src/Accordion/scopeQuery.ts +7 -0
  25. package/src/Accordion/styles.css +21 -0
  26. package/src/CheckBox/CheckBox.tsx +41 -0
  27. package/src/CheckBox/index.ts +1 -0
  28. package/src/ComboBox/ComboBox.story.tsx +118 -0
  29. package/src/ComboBox/Combobox.tsx +153 -0
  30. package/src/ComboBox/ComboboxButton.tsx +60 -0
  31. package/src/ComboBox/ComboboxInput.tsx +178 -0
  32. package/src/ComboBox/ComboboxLabel.tsx +32 -0
  33. package/src/ComboBox/ComboboxList.tsx +47 -0
  34. package/src/ComboBox/ComboboxOption.tsx +107 -0
  35. package/src/ComboBox/ComboboxPopover.tsx +58 -0
  36. package/src/ComboBox/cities.ts +23194 -0
  37. package/src/ComboBox/context.ts +33 -0
  38. package/src/ComboBox/hooks.tsx +428 -0
  39. package/src/ComboBox/index.ts +8 -0
  40. package/src/ComboBox/makeHash.ts +19 -0
  41. package/src/ComboBox/scopeQuery.ts +6 -0
  42. package/src/ComboBox/styles.css +30 -0
  43. package/src/FocusLock/FocusLock.tsx +59 -0
  44. package/src/FocusLock/index.ts +1 -0
  45. package/src/FocusLock/tabUtils.ts +28 -0
  46. package/src/FocusLock/useFocusLock.ts +61 -0
  47. package/src/List/List.story.tsx +17 -0
  48. package/src/List/List.tsx +17 -0
  49. package/src/List/ListItem.tsx +23 -0
  50. package/src/List/context.ts +19 -0
  51. package/src/List/index.ts +2 -0
  52. package/src/Menu/.gitkeep +0 -0
  53. package/src/Menu/Menu.story.tsx +158 -0
  54. package/src/Menu/Menu.tsx +60 -0
  55. package/src/Menu/MenuButton.tsx +83 -0
  56. package/src/Menu/MenuItem.tsx +83 -0
  57. package/src/Menu/MenuList.tsx +201 -0
  58. package/src/Menu/MenuPopover.tsx +25 -0
  59. package/src/Menu/context.ts +32 -0
  60. package/src/Menu/index.ts +5 -0
  61. package/src/Menu/scope.ts +7 -0
  62. package/src/Menu/styles.css +42 -0
  63. package/src/Modal/Modal.story.tsx +242 -0
  64. package/src/Modal/Modal.tsx +42 -0
  65. package/src/Modal/ModalBackdrop.tsx +72 -0
  66. package/src/Modal/NavDrawer.story.tsx +157 -0
  67. package/src/Modal/index.ts +2 -0
  68. package/src/Modal/styles.css +46 -0
  69. package/src/Popover/.gitkeep +0 -0
  70. package/src/Popper/Popper.story.tsx +267 -0
  71. package/src/Popper/Popper.tsx +149 -0
  72. package/src/Popper/PopperArrow.tsx +36 -0
  73. package/src/Popper/context.ts +9 -0
  74. package/src/Popper/index.ts +3 -0
  75. package/src/Popper/styles.css +60 -0
  76. package/src/Portal/Portal.tsx +20 -0
  77. package/src/Portal/index.ts +1 -0
  78. package/src/RadioButton/RadioButton.story.tsx +73 -0
  79. package/src/RadioButton/RadioButton.tsx +48 -0
  80. package/src/RadioButton/RadioGroup.tsx +56 -0
  81. package/src/RadioButton/context.ts +19 -0
  82. package/src/RadioButton/index.ts +2 -0
  83. package/src/SkipNav/SkipNav.tsx +16 -0
  84. package/src/SkipNav/index.tsx +1 -0
  85. package/src/Spinner/Spinner.story.tsx +30 -0
  86. package/src/Spinner/Spinner.tsx +112 -0
  87. package/src/Spinner/SpinnerButton.tsx +48 -0
  88. package/src/Spinner/context.ts +21 -0
  89. package/src/Spinner/index.ts +2 -0
  90. package/src/Spinner/styles.css +23 -0
  91. package/src/Tabs/Tab.story.tsx +78 -0
  92. package/src/Tabs/Tab.tsx +131 -0
  93. package/src/Tabs/TabList.tsx +63 -0
  94. package/src/Tabs/TabPanel.tsx +52 -0
  95. package/src/Tabs/TabPanels.tsx +30 -0
  96. package/src/Tabs/Tabs.tsx +47 -0
  97. package/src/Tabs/context.ts +30 -0
  98. package/src/Tabs/index.tsx +5 -0
  99. package/src/Tabs/scopeQuery.ts +6 -0
  100. package/src/Tabs/styles.css +0 -0
  101. package/src/Tooltip/.gitkeep +0 -0
  102. package/src/Tooltip/Tooltip.story.tsx +59 -0
  103. package/src/Tooltip/Tooltip.tsx +48 -0
  104. package/src/Tooltip/index.ts +1 -0
  105. package/src/Tooltip/stateMachine.ts +196 -0
  106. package/src/Tooltip/styles.css +17 -0
  107. package/src/Tooltip/useTooltip.ts +128 -0
  108. package/src/hooks/index.ts +14 -0
  109. package/src/hooks/useAutoFocus.ts +13 -0
  110. package/src/hooks/useChildrenCounter.ts +50 -0
  111. package/src/hooks/useControlledState.ts +37 -0
  112. package/src/hooks/useFocusReturn.ts +23 -0
  113. package/src/hooks/useFocusState.ts +28 -0
  114. package/src/hooks/useGestureHandlers.ts +217 -0
  115. package/src/hooks/useId.ts +18 -0
  116. package/src/hooks/useMeasure.ts +33 -0
  117. package/src/hooks/useOnClickOutside.ts +32 -0
  118. package/src/hooks/useOnKeyDown.ts +18 -0
  119. package/src/hooks/useReducerMachine.ts +59 -0
  120. package/src/hooks/useRemoveBodyScroll.ts +37 -0
  121. package/src/hooks/useScope.ts +51 -0
  122. package/src/hooks/useThrottle.ts +19 -0
  123. package/src/index.ts +19 -0
  124. package/src/utils/assignRef.ts +27 -0
  125. package/src/utils/clamp.ts +3 -0
  126. package/src/utils/createSubscription.ts +16 -0
  127. package/src/utils/getCircularIndex.ts +7 -0
  128. package/src/utils/index.ts +4 -0
  129. package/src/utils/rubberBandClamp.ts +25 -0
  130. package/src/utils/wrapEvent.ts +20 -0
@@ -0,0 +1,56 @@
1
+ import { forwardRef } from 'react';
2
+ import type * as React from 'react';
3
+ import { RadioGroupProvider, RadioValue } from './context';
4
+ import { useControlledState, useId } from '../hooks';
5
+
6
+ export interface RadioGroupProps
7
+ extends Omit<
8
+ React.HTMLAttributes<HTMLDivElement>,
9
+ 'onChange' | 'value' | 'defaultValue'
10
+ > {
11
+ as?: React.ElementType<any>;
12
+ innerAs?: React.ElementType<any>;
13
+ children?: React.ReactNode;
14
+ value?: RadioValue;
15
+ defaultValue?: RadioValue;
16
+ name?: string;
17
+ onChange?: (
18
+ e: React.ChangeEvent<HTMLInputElement>,
19
+ value: RadioValue
20
+ ) => void;
21
+ }
22
+
23
+ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
24
+ function RadioGroup(props, forwardedRef) {
25
+ const {
26
+ as: Comp = 'div',
27
+ onChange: onChangeProp,
28
+ value: valueProp,
29
+ name: nameProp,
30
+ defaultValue,
31
+ ...otherProps
32
+ } = props;
33
+ const [value, onChange] = useControlledState(
34
+ valueProp,
35
+ onChangeProp,
36
+ defaultValue,
37
+ (setValue) => (e, value) => {
38
+ setValue(value);
39
+ }
40
+ );
41
+
42
+ const fallbackId = useId();
43
+
44
+ return (
45
+ <RadioGroupProvider
46
+ value={{
47
+ value,
48
+ onChange,
49
+ name: nameProp || fallbackId,
50
+ }}
51
+ >
52
+ <Comp ref={forwardedRef} role="radiogroup" {...otherProps} />
53
+ </RadioGroupProvider>
54
+ );
55
+ }
56
+ );
@@ -0,0 +1,19 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export type RadioValue = string | number | boolean;
4
+
5
+ // RadioGroup Component
6
+ export interface RadioGroupContextProps {
7
+ value: RadioValue | undefined;
8
+ onChange?: (
9
+ e: React.ChangeEvent<HTMLInputElement>,
10
+ value: RadioValue
11
+ ) => void;
12
+ name: string | undefined;
13
+ }
14
+
15
+ const RadioGroupContext = createContext<RadioGroupContextProps | undefined>(
16
+ undefined
17
+ );
18
+ export const { Provider: RadioGroupProvider } = RadioGroupContext;
19
+ export const useRadioGroupContext = () => useContext(RadioGroupContext);
@@ -0,0 +1,2 @@
1
+ export * from './RadioButton';
2
+ export * from './RadioGroup';
@@ -0,0 +1,16 @@
1
+ import { forwardRef } from 'react';
2
+ import type * as React from 'react';
3
+
4
+ export interface SkipNavProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ as?: React.ElementType<any>;
6
+ innerAs?: React.ElementType<any>;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export const SkipNav = forwardRef<HTMLDivElement, SkipNavProps>(
11
+ function SkipNav(props, forwardedRef) {
12
+ const { as: Comp = 'div', ...otherProps } = props;
13
+
14
+ return <Comp ref={forwardedRef} {...otherProps} />;
15
+ }
16
+ );
@@ -0,0 +1 @@
1
+ export * from './SkipNav';
@@ -0,0 +1,30 @@
1
+ import { useState } from 'react';
2
+ import { Spinner, SpinnerButton } from './';
3
+ import { storiesOf } from '@storybook/react';
4
+ import './styles.css';
5
+
6
+ const stories = storiesOf('Components/Spinner', module);
7
+
8
+ const Example = ({ vertical = false }) => {
9
+ const [value, setValue] = useState(0);
10
+
11
+ return (
12
+ <Spinner
13
+ value={value}
14
+ minValue={0}
15
+ maxValue={5}
16
+ onChange={(e, value) => setValue(value)}
17
+ style={{
18
+ flexDirection: vertical ? 'column-reverse' : 'row',
19
+ }}
20
+ >
21
+ <SpinnerButton type="previous">-</SpinnerButton>
22
+ <div>{value}</div>
23
+ <SpinnerButton type="next">+</SpinnerButton>
24
+ </Spinner>
25
+ );
26
+ };
27
+
28
+ stories.add('horizontal', () => <Example />);
29
+
30
+ stories.add('vertical', () => <Example vertical={true} />);
@@ -0,0 +1,112 @@
1
+ import { forwardRef, useState, useRef } from 'react';
2
+ import type * as React from 'react';
3
+ import { SpinnerProvider } from './context';
4
+ import { clamp as clampFunc } from '../utils/clamp';
5
+ import { wrapEvent, assignMultipleRefs } from '../utils';
6
+
7
+ export interface SpinnerProps
8
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
9
+ as?: React.ElementType<any>;
10
+ innerAs?: React.ElementType<any>;
11
+ children?: React.ReactNode;
12
+ value?: number;
13
+ minValue?: number;
14
+ maxValue?: number;
15
+ stepSize?: number;
16
+ onChange?: (
17
+ e:
18
+ | React.MouseEvent<HTMLButtonElement>
19
+ | React.KeyboardEvent<HTMLDivElement>,
20
+ value: number
21
+ ) => void;
22
+ }
23
+
24
+ export const Spinner = forwardRef<HTMLDivElement, SpinnerProps>(
25
+ function Spinner(props, forwardedRef) {
26
+ const {
27
+ as: Comp = 'div',
28
+ minValue = -1000,
29
+ maxValue = 1000,
30
+ stepSize = 10,
31
+ value = 0,
32
+ onChange,
33
+ onKeyDown,
34
+ onBlur,
35
+ onFocus,
36
+ ...otherProps
37
+ } = props;
38
+ const [spinnerHasFocus, setSpinnerHasFocus] = useState(false);
39
+ const ref = useRef<HTMLDivElement>();
40
+
41
+ const clamp = (value: number) => clampFunc(value, minValue, maxValue);
42
+
43
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
44
+ let nextValue = value;
45
+ switch (e.key) {
46
+ case 'ArrowUp':
47
+ nextValue += 1;
48
+ break;
49
+ case 'ArrowDown':
50
+ nextValue -= 1;
51
+ break;
52
+ case 'Home':
53
+ nextValue = minValue;
54
+ break;
55
+ case 'End':
56
+ nextValue = maxValue;
57
+ break;
58
+ case 'PageUp':
59
+ nextValue += stepSize;
60
+ break;
61
+ case 'PageDown':
62
+ nextValue -= stepSize;
63
+ break;
64
+ default:
65
+ return;
66
+ }
67
+
68
+ ref.current && ref.current.focus();
69
+ nextValue = clamp(nextValue);
70
+ if (nextValue !== value) {
71
+ e.preventDefault(); // prevent scrolling
72
+ onChange && onChange(e, nextValue);
73
+ }
74
+ };
75
+
76
+ const handleFocus = () => {
77
+ setSpinnerHasFocus(true);
78
+ };
79
+
80
+ const handleBlur = () => {
81
+ setSpinnerHasFocus(false);
82
+ };
83
+
84
+ return (
85
+ <SpinnerProvider
86
+ value={{
87
+ minValue,
88
+ value,
89
+ maxValue,
90
+ stepSize,
91
+ onChange,
92
+ clamp,
93
+ spinnerHasFocus,
94
+ }}
95
+ >
96
+ <Comp
97
+ ref={assignMultipleRefs(forwardedRef, ref)}
98
+ data-spinner-root=""
99
+ role="spinbutton"
100
+ aria-valuenow={value}
101
+ aria-valuemin={minValue}
102
+ aria-valuemax={maxValue}
103
+ tabIndex={0}
104
+ onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
105
+ onFocus={wrapEvent(onFocus, handleFocus)}
106
+ onBlur={wrapEvent(onBlur, handleBlur)}
107
+ {...otherProps}
108
+ />
109
+ </SpinnerProvider>
110
+ );
111
+ }
112
+ );
@@ -0,0 +1,48 @@
1
+ import { forwardRef } from 'react';
2
+ import type * as React from 'react';
3
+ import { useSpinnerContext } from './context';
4
+ import { wrapEvent } from '../utils';
5
+
6
+ export interface SpinnerButtonProps
7
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
8
+ as?: React.ElementType<any>;
9
+ innerAs?: React.ElementType<any>;
10
+ children?: React.ReactNode;
11
+ type: 'next' | 'previous';
12
+ }
13
+
14
+ export const SpinnerButton = forwardRef<HTMLButtonElement, SpinnerButtonProps>(
15
+ function SpinnerButton(props, forwardedRef) {
16
+ const { as: Comp = 'button', type, onClick, ...otherProps } = props;
17
+ const spinnerContext = useSpinnerContext();
18
+
19
+ if (!spinnerContext) {
20
+ throw new Error('Missing <Spinner /> in component tree');
21
+ }
22
+
23
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
24
+ const delta = type === 'next' ? 1 : -1;
25
+
26
+ spinnerContext.onChange &&
27
+ spinnerContext.onChange(e, spinnerContext.value + delta);
28
+ };
29
+
30
+ const disabled =
31
+ type === 'next'
32
+ ? spinnerContext.value + 1 > spinnerContext.maxValue
33
+ : spinnerContext.value - 1 < spinnerContext.minValue;
34
+
35
+ return (
36
+ <Comp
37
+ {...otherProps}
38
+ type="button"
39
+ ref={forwardedRef}
40
+ tabIndex={-1}
41
+ data-spinner-button=""
42
+ data-spinner-has-focus={spinnerContext.spinnerHasFocus ? '' : undefined}
43
+ disabled={disabled}
44
+ onClick={wrapEvent(onClick, handleClick)}
45
+ />
46
+ );
47
+ }
48
+ );
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ // Spinner Component
4
+ export interface SpinnerContextProps {
5
+ value: number;
6
+ minValue: number;
7
+ maxValue: number;
8
+ stepSize: number;
9
+ onChange?: (
10
+ e:
11
+ | React.MouseEvent<HTMLButtonElement>
12
+ | React.KeyboardEvent<HTMLDivElement>,
13
+ value: number
14
+ ) => void;
15
+ clamp: (value: number) => number;
16
+ spinnerHasFocus: boolean;
17
+ }
18
+
19
+ const spinbuttonContext = createContext<SpinnerContextProps | null>(null);
20
+ export const { Provider: SpinnerProvider } = spinbuttonContext;
21
+ export const useSpinnerContext = () => useContext(spinbuttonContext);
@@ -0,0 +1,2 @@
1
+ export * from './Spinner';
2
+ export * from './SpinnerButton';
@@ -0,0 +1,23 @@
1
+ [data-spinner-root] {
2
+ display: inline-flex;
3
+ }
4
+
5
+ [data-spinner-root] > div {
6
+ padding: 4px;
7
+ text-align: center;
8
+ min-width: 25px;
9
+ }
10
+
11
+ [data-spinner-button] {
12
+ background-color: #ddd;
13
+ box-sizing: border-box;
14
+ border-width: 1px;
15
+ border-color: transparent;
16
+ border-radius: 4px;
17
+ border-style: solid;
18
+ width: 45px;
19
+ }
20
+
21
+ [data-spinner-button][data-spinner-has-focus]:not([disabled]) {
22
+ border-color: gray;
23
+ }
@@ -0,0 +1,78 @@
1
+ import { useState } from 'react';
2
+ import { Tab, TabPanel, TabPanels, Tabs, TabList } from './';
3
+ import { storiesOf } from '@storybook/react';
4
+ import './styles.css';
5
+
6
+ const stories = storiesOf('Components/Tab', module);
7
+
8
+ export const ExampleHorizontal = ({ manualActivation = false }) => {
9
+ const [index, setIndex] = useState(0);
10
+ return (
11
+ <Tabs index={index} onChange={(e, idx) => setIndex(idx)}>
12
+ <TabList manualActivation={manualActivation}>
13
+ <Tab>One</Tab>
14
+ <Tab>Two</Tab>
15
+ <Tab>Three</Tab>
16
+ </TabList>
17
+
18
+ <TabPanels>
19
+ <TabPanel lazy>
20
+ <h1>one!</h1>
21
+ <button>yo</button>
22
+ </TabPanel>
23
+ <TabPanel lazy>
24
+ <h1>two!</h1>
25
+ </TabPanel>
26
+ <TabPanel lazy>
27
+ <h1>three!</h1>
28
+ </TabPanel>
29
+ </TabPanels>
30
+ </Tabs>
31
+ );
32
+ };
33
+
34
+ export const ExampleVertical = ({ manualActivation = false }) => {
35
+ const [index, setIndex] = useState(0);
36
+ return (
37
+ <Tabs index={index} onChange={(e, idx) => setIndex(idx)}>
38
+ <div style={{ display: 'flex' }}>
39
+ <TabList
40
+ manualActivation={manualActivation}
41
+ vertical={true}
42
+ style={{
43
+ display: 'flex',
44
+ flexDirection: 'column',
45
+ marginRight: 16,
46
+ }}
47
+ >
48
+ <Tab>1</Tab>
49
+ <Tab>2</Tab>
50
+ <Tab>3</Tab>
51
+ </TabList>
52
+
53
+ <TabPanels>
54
+ <TabPanel>
55
+ <h1>one!</h1>
56
+ <button>yo</button>
57
+ </TabPanel>
58
+ <TabPanel>
59
+ <h1>two!</h1>
60
+ </TabPanel>
61
+ <TabPanel>
62
+ <h1>three!</h1>
63
+ </TabPanel>
64
+ </TabPanels>
65
+ </div>
66
+ </Tabs>
67
+ );
68
+ };
69
+
70
+ stories.add('horizontal, controlled', () => <ExampleHorizontal />);
71
+ stories.add('horizontal, controlled, manual activation', () => (
72
+ <ExampleHorizontal manualActivation={true} />
73
+ ));
74
+
75
+ stories.add('vertical, controlled', () => <ExampleVertical />);
76
+ stories.add('vertical, controlled, manual activation', () => (
77
+ <ExampleVertical manualActivation={true} />
78
+ ));
@@ -0,0 +1,131 @@
1
+ import { forwardRef, useRef } from 'react';
2
+ import type * as React from 'react';
3
+ import { useTabsContext, useTabListContext } from './context';
4
+ import { assignMultipleRefs, getCircularIndex, wrapEvent } from '../utils';
5
+ import { scopeQuery } from './scopeQuery';
6
+
7
+ function getNextIndex(
8
+ desiredIndex: number,
9
+ delta: number,
10
+ allTabs: HTMLButtonElement[]
11
+ ) {
12
+ for (let i = 0; i < allTabs.length; i++) {
13
+ const nextIndex = getCircularIndex(
14
+ desiredIndex + delta * i,
15
+ allTabs.length
16
+ );
17
+
18
+ if (nextIndex !== null && !allTabs[nextIndex].disabled) {
19
+ return nextIndex;
20
+ }
21
+ }
22
+
23
+ return null;
24
+ }
25
+
26
+ export interface TabProps
27
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
28
+ as?: React.ElementType<any>;
29
+ innerAs?: React.ElementType<any>;
30
+ children?: React.ReactNode;
31
+ }
32
+
33
+ export const Tab = forwardRef<HTMLButtonElement, TabProps>(function Tab(
34
+ props,
35
+ forwardedRef
36
+ ) {
37
+ const {
38
+ as: Comp = 'button',
39
+ onKeyDown,
40
+ onClick,
41
+ index: tabIndex = -1,
42
+ ...otherProps
43
+ } = props as TabProps & { index: number };
44
+ const tabContext = useTabsContext();
45
+ const tabListContext = useTabListContext();
46
+
47
+ const ref = useRef<HTMLButtonElement | undefined>(undefined);
48
+
49
+ if (!tabContext || !tabListContext) {
50
+ throw new Error('Missing <Tabs /> or <TabList /> in the component tree');
51
+ }
52
+
53
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
54
+ const right = tabListContext.vertical ? 'ArrowDown' : 'ArrowRight';
55
+ const left = tabListContext.vertical ? 'ArrowUp' : 'ArrowLeft';
56
+ const first = 'Home';
57
+ const last = 'End';
58
+
59
+ const navigateIndex = (desiredIndex: number, isLast: boolean) => {
60
+ const delta = e.key === right || e.key === first ? 1 : -1;
61
+
62
+ const allTabs = tabListContext.tabsScope.current.queryAllNodes(
63
+ scopeQuery
64
+ );
65
+ const currentIndex = ref.current ? allTabs.indexOf(ref.current) : -1;
66
+
67
+ const nextIndex = getNextIndex(
68
+ isLast ? desiredIndex : currentIndex + desiredIndex,
69
+ delta,
70
+ allTabs as HTMLButtonElement[]
71
+ );
72
+
73
+ if (
74
+ nextIndex !== null &&
75
+ nextIndex !== currentIndex &&
76
+ tabContext.onChange
77
+ ) {
78
+ allTabs[nextIndex].focus();
79
+ !tabListContext.manualActivation && tabContext.onChange(e, nextIndex);
80
+ }
81
+ };
82
+
83
+ switch (e.key) {
84
+ case right:
85
+ case left: {
86
+ navigateIndex(e.key === right ? 1 : -1, false);
87
+ break;
88
+ }
89
+ case first: // Home / End
90
+ case last: {
91
+ navigateIndex(e.key === first ? 0 : -1, true);
92
+ break;
93
+ }
94
+ }
95
+ };
96
+
97
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
98
+ const allTabs = tabListContext.tabsScope.current.queryAllNodes(scopeQuery);
99
+ const currentIndex = ref.current ? allTabs.indexOf(ref.current) : -1;
100
+
101
+ if (currentIndex >= 0) {
102
+ tabContext.onChange && tabContext.onChange(e, currentIndex);
103
+ }
104
+ };
105
+
106
+ const isSelected = tabIndex === tabContext.currentIndex;
107
+
108
+ return (
109
+ <Comp
110
+ {...otherProps}
111
+ ref={assignMultipleRefs(ref, forwardedRef)}
112
+ data-tab=""
113
+ id={
114
+ tabContext.tabListId !== null && tabIndex >= 0
115
+ ? `tab-${tabContext.tabListId}-${tabIndex}`
116
+ : undefined
117
+ }
118
+ aria-controls={
119
+ tabContext.tabListId !== null && tabIndex >= 0
120
+ ? `tabpanel-${tabContext.tabListId}-${tabIndex}`
121
+ : undefined
122
+ }
123
+ role="tab"
124
+ tabIndex={isSelected ? 0 : -1}
125
+ aria-selected={isSelected}
126
+ selected={isSelected}
127
+ onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
128
+ onClick={wrapEvent(onClick, handleClick)}
129
+ />
130
+ );
131
+ });
@@ -0,0 +1,63 @@
1
+ import { forwardRef, useRef, useEffect, Children, cloneElement } from 'react';
2
+ import type * as React from 'react';
3
+ import { TabListProvider, useTabsContext } from './context';
4
+ import { useScope, useId } from '../hooks';
5
+ import { assignMultipleRefs } from '../utils';
6
+
7
+ export interface TabListProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ as?: React.ElementType<any>;
9
+ innerAs?: React.ElementType<any>;
10
+ manualActivation?: boolean;
11
+ vertical?: boolean;
12
+ children?: React.ReactNode;
13
+ }
14
+
15
+ export const TabList = forwardRef<HTMLDivElement, TabListProps>(
16
+ function TabList(props, forwardedRef) {
17
+ const {
18
+ as: Comp = 'div',
19
+ manualActivation = false,
20
+ vertical = false,
21
+ children: childrenProps,
22
+ ...otherProps
23
+ } = props;
24
+
25
+ const ref = useRef();
26
+ const tabsScope = useScope(ref);
27
+ const tabsContext = useTabsContext();
28
+ const id = useId();
29
+
30
+ if (!tabsContext) {
31
+ throw new Error('Missing <Tabs /> in the component tree');
32
+ }
33
+
34
+ useEffect(() => {
35
+ if (id !== undefined) {
36
+ tabsContext.setTabListId(id);
37
+
38
+ return () => {
39
+ tabsContext.setTabListId(null);
40
+ };
41
+ }
42
+
43
+ return;
44
+ }, [id, tabsContext, tabsContext.setTabListId]);
45
+
46
+ const children = Children.map(childrenProps, (node, index) =>
47
+ cloneElement(node as any, { index })
48
+ );
49
+
50
+ return (
51
+ <TabListProvider value={{ tabsScope, manualActivation, vertical }}>
52
+ <Comp
53
+ ref={assignMultipleRefs(forwardedRef, ref)}
54
+ data-tab-list=""
55
+ role="tablist"
56
+ {...otherProps}
57
+ >
58
+ {children}
59
+ </Comp>
60
+ </TabListProvider>
61
+ );
62
+ }
63
+ );
@@ -0,0 +1,52 @@
1
+ import { forwardRef } from 'react';
2
+ import type * as React from 'react';
3
+ import { useTabsContext } from './context';
4
+
5
+ export interface TabPanelProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ as?: React.ElementType<any>;
7
+ innerAs?: React.ElementType<any>;
8
+ children?: React.ReactNode;
9
+ lazy?: boolean;
10
+ }
11
+
12
+ export const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(
13
+ function TabPanel(props, forwardedRef) {
14
+ const {
15
+ as: Comp = 'div',
16
+ index,
17
+ lazy = false,
18
+ children,
19
+ ...otherProps
20
+ } = props as TabPanelProps & { index: number };
21
+ const tabsContext = useTabsContext();
22
+
23
+ if (!tabsContext) {
24
+ throw new Error('Missing <Tabs /> in the component tree.');
25
+ }
26
+
27
+ const isSelected = tabsContext.currentIndex === index;
28
+
29
+ return (
30
+ <Comp
31
+ ref={forwardedRef}
32
+ data-tab-panel=""
33
+ role="tabpanel"
34
+ id={
35
+ tabsContext.tabListId !== null
36
+ ? `tabpanel-${tabsContext.tabListId}-${index}`
37
+ : undefined
38
+ }
39
+ aria-labelledby={
40
+ tabsContext.tabListId !== null
41
+ ? `tab-${tabsContext.tabListId}-${index}`
42
+ : undefined
43
+ }
44
+ hidden={isSelected ? undefined : 'hidden'}
45
+ tabIndex={0}
46
+ {...otherProps}
47
+ >
48
+ {(isSelected || !lazy) && children}
49
+ </Comp>
50
+ );
51
+ }
52
+ );