@tcn/ui 0.6.0 → 0.8.0
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/dist/draggable.module-BgelQsuJ.js +5 -0
- package/dist/draggable.module-BgelQsuJ.js.map +1 -0
- package/dist/form/field/field.js +13 -10
- package/dist/form/field/field.js.map +1 -1
- package/dist/form/field_presenters/field_presenter.d.ts +2 -2
- package/dist/form/field_presenters/field_presenter.d.ts.map +1 -1
- package/dist/form/field_presenters/field_presenter.js.map +1 -1
- package/dist/inputs/color_input/color_picker.js +5 -3
- package/dist/inputs/color_input/color_picker.js.map +1 -1
- package/dist/inputs/combo_box/combo_box.js +4 -2
- package/dist/inputs/combo_box/combo_box.js.map +1 -1
- package/dist/inputs/date_picker/date_picker.js +16 -14
- package/dist/inputs/date_picker/date_picker.js.map +1 -1
- package/dist/inputs/date_picker/date_picker_input.js +10 -8
- package/dist/inputs/date_picker/date_picker_input.js.map +1 -1
- package/dist/inputs/date_picker/date_picker_year_selector.js +4 -2
- package/dist/inputs/date_picker/date_picker_year_selector.js.map +1 -1
- package/dist/inputs/mask_input/key_capture_input.js +15 -12
- package/dist/inputs/mask_input/key_capture_input.js.map +1 -1
- package/dist/inputs/mask_input/mask_input.js +13 -10
- package/dist/inputs/mask_input/mask_input.js.map +1 -1
- package/dist/inputs/multiselect/multiselect.js +9 -7
- package/dist/inputs/multiselect/multiselect.js.map +1 -1
- package/dist/inputs/phone_number_input/phone_number_input.d.ts +1 -0
- package/dist/inputs/phone_number_input/phone_number_input.d.ts.map +1 -1
- package/dist/inputs/phone_number_input/phone_number_input.js +136 -133
- package/dist/inputs/phone_number_input/phone_number_input.js.map +1 -1
- package/dist/inputs/select/select.js +4 -2
- package/dist/inputs/select/select.js.map +1 -1
- package/dist/inputs/slider/slider.js +7 -5
- package/dist/inputs/slider/slider.js.map +1 -1
- package/dist/inputs/suggestions/suggestion_list.js +4 -2
- package/dist/inputs/suggestions/suggestion_list.js.map +1 -1
- package/dist/inputs/switch/switch.js +16 -14
- package/dist/inputs/switch/switch.js.map +1 -1
- package/dist/inputs/unit_input/unit_input.js +16 -14
- package/dist/inputs/unit_input/unit_input.js.map +1 -1
- package/dist/navigation/tabs/primitives/tabs_list.d.ts.map +1 -1
- package/dist/navigation/tabs/primitives/tabs_list.js +61 -21
- package/dist/navigation/tabs/primitives/tabs_list.js.map +1 -1
- package/dist/navigation/tabs/state/link/tab_link.d.ts.map +1 -1
- package/dist/navigation/tabs/state/link/tab_link.js +25 -19
- package/dist/navigation/tabs/state/link/tab_link.js.map +1 -1
- package/dist/navigation/tabs/state/link/use_tab_link.js +8 -8
- package/dist/navigation/tabs/state/link/use_tab_link.js.map +1 -1
- package/dist/navigation/tabs/state/tab.d.ts.map +1 -1
- package/dist/navigation/tabs/state/tab.js +8 -3
- package/dist/navigation/tabs/state/tab.js.map +1 -1
- package/dist/overlay/caret/caret.d.ts +8 -0
- package/dist/overlay/caret/caret.d.ts.map +1 -0
- package/dist/overlay/caret/caret.js +20 -0
- package/dist/overlay/caret/caret.js.map +1 -0
- package/dist/overlay/menu/menu.js +34 -32
- package/dist/overlay/menu/menu.js.map +1 -1
- package/dist/overlay/popper/legacy/popper.js +22 -20
- package/dist/overlay/popper/legacy/popper.js.map +1 -1
- package/dist/overlay/popper/preview_popper.js +12 -9
- package/dist/overlay/popper/preview_popper.js.map +1 -1
- package/dist/overlay/tethered/hooks/calculate_origin.d.ts +23 -0
- package/dist/overlay/tethered/hooks/calculate_origin.d.ts.map +1 -0
- package/dist/overlay/tethered/hooks/calculate_origin.js +41 -0
- package/dist/overlay/tethered/hooks/calculate_origin.js.map +1 -0
- package/dist/overlay/tethered/hooks/useCaretRefDimensions.d.ts +9 -0
- package/dist/overlay/tethered/hooks/useCaretRefDimensions.d.ts.map +1 -0
- package/dist/overlay/tethered/hooks/useCaretRefDimensions.js +14 -0
- package/dist/overlay/tethered/hooks/useCaretRefDimensions.js.map +1 -0
- package/dist/overlay/tethered/hooks/useTether.d.ts +1 -1
- package/dist/overlay/tethered/hooks/useTether.d.ts.map +1 -1
- package/dist/overlay/tethered/hooks/useTether.js +22 -21
- package/dist/overlay/tethered/hooks/useTether.js.map +1 -1
- package/dist/overlay/tethered/hooks/useTetherContentRect.d.ts +3 -0
- package/dist/overlay/tethered/hooks/useTetherContentRect.d.ts.map +1 -0
- package/dist/overlay/tethered/hooks/useTetherContentRect.js +36 -0
- package/dist/overlay/tethered/hooks/useTetherContentRect.js.map +1 -0
- package/dist/overlay/tethered/hooks/useTetherOrigin.d.ts +14 -0
- package/dist/overlay/tethered/hooks/useTetherOrigin.d.ts.map +1 -0
- package/dist/overlay/tethered/hooks/useTetherOrigin.js +24 -0
- package/dist/overlay/tethered/hooks/useTetherOrigin.js.map +1 -0
- package/dist/overlay/tethered/tethered.d.ts +2 -1
- package/dist/overlay/tethered/tethered.d.ts.map +1 -1
- package/dist/overlay/tethered/tethered.js +71 -38
- package/dist/overlay/tethered/tethered.js.map +1 -1
- package/dist/stacks/box/box.js +29 -27
- package/dist/stacks/box/box.js.map +1 -1
- package/dist/stacks/h_collapsible_box.js +14 -12
- package/dist/stacks/h_collapsible_box.js.map +1 -1
- package/dist/stacks/v_collapsible_box.js +8 -6
- package/dist/stacks/v_collapsible_box.js.map +1 -1
- package/dist/surfaces/pop_confirm/pop_confirm.d.ts.map +1 -1
- package/dist/surfaces/pop_confirm/pop_confirm.js +14 -13
- package/dist/surfaces/pop_confirm/pop_confirm.js.map +1 -1
- package/dist/surfaces/tooltip/tooltip.d.ts.map +1 -1
- package/dist/surfaces/tooltip/tooltip.js +12 -11
- package/dist/surfaces/tooltip/tooltip.js.map +1 -1
- package/dist/tethered.css +1 -1
- package/dist/themes/themes/ergo/ergo_theme.css +1 -1
- package/dist/themes/themes/ergo/ergo_theme.js +87 -57
- package/dist/themes/themes/ergo/ergo_theme.js.map +1 -1
- package/dist/themes/themes/windows_98/windows_98.css +1 -1
- package/dist/themes/themes/windows_98/windows_98_theme.js +18 -18
- package/dist/themes/themes/windows_98/windows_98_theme.js.map +1 -1
- package/dist/utils/dnd/draggable/draggable.js +13 -12
- package/dist/utils/dnd/draggable/draggable.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +23 -19
- package/dist/utils/index.js.map +1 -1
- package/package.json +11 -11
- package/src/form/field_presenters/field_presenter.ts +3 -3
- package/src/inputs/phone_number_input/phone_number_input.stories.tsx +24 -0
- package/src/inputs/phone_number_input/phone_number_input.tsx +8 -6
- package/src/navigation/tabs/primitives/tabs_list.tsx +46 -2
- package/src/navigation/tabs/state/link/tab_link.tsx +4 -1
- package/src/navigation/tabs/state/link/use_tab_link.ts +4 -4
- package/src/navigation/tabs/state/tab.tsx +10 -0
- package/src/overlay/{carrot/carrot.stories.tsx → caret/caret.stories.tsx} +14 -14
- package/src/overlay/caret/caret.tsx +24 -0
- package/src/overlay/tethered/__stories__/shared/base_story_config.ts +8 -0
- package/src/overlay/tethered/hooks/calculate_origin.ts +74 -0
- package/src/overlay/tethered/hooks/useCaretRefDimensions.ts +22 -0
- package/src/overlay/tethered/hooks/useTether.ts +4 -3
- package/src/overlay/tethered/hooks/useTetherContentRect.ts +49 -0
- package/src/overlay/tethered/hooks/useTetherOrigin.ts +49 -0
- package/src/overlay/tethered/tethered.module.css +55 -0
- package/src/overlay/tethered/tethered.tsx +44 -6
- package/src/surfaces/panel/__stories__/panel.stories.tsx +62 -27
- package/src/surfaces/panel/__stories__/panel_stories.module.css +14 -1
- package/src/surfaces/pop_confirm/pop_confirm.tsx +4 -3
- package/src/surfaces/tooltip/tooltip.tsx +1 -0
- package/src/themes/themes/ergo/ergo_theme.css +87 -57
- package/src/themes/themes/windows_98/windows_98.css +18 -18
- package/src/utils/index.ts +3 -0
- package/dist/overlay/carrot/base_carrot.d.ts +0 -8
- package/dist/overlay/carrot/base_carrot.d.ts.map +0 -1
- package/dist/overlay/carrot/base_carrot.js +0 -21
- package/dist/overlay/carrot/base_carrot.js.map +0 -1
- package/src/overlay/carrot/base_carrot.tsx +0 -24
package/dist/utils/index.js
CHANGED
|
@@ -1,33 +1,37 @@
|
|
|
1
1
|
import { ClickAwayListener as o, isEventWithinElement as t } from "./click_away_listener.js";
|
|
2
|
-
import { FocusRedirect as
|
|
3
|
-
import { ScrollAwayListener as
|
|
4
|
-
import { useDraggable as
|
|
2
|
+
import { FocusRedirect as a } from "./focus_redirect.js";
|
|
3
|
+
import { ScrollAwayListener as p } from "./scroll_away_listener.js";
|
|
4
|
+
import { useDraggable as s } from "./dnd/hooks/use_draggable.js";
|
|
5
5
|
import { makeContextHook as i } from "./hooks/make_context_hook.js";
|
|
6
|
-
import { useForkRef as
|
|
6
|
+
import { useForkRef as l } from "./hooks/use_fork_ref.js";
|
|
7
7
|
import { useMediaQuery as d } from "./hooks/use_media_query.js";
|
|
8
|
-
import { TriggerConfig as
|
|
9
|
-
import { defaultValue as
|
|
10
|
-
import { CalendarDatesGenerator as
|
|
11
|
-
import { getDaysOfWeek as
|
|
12
|
-
import { getMonthsOfYear as
|
|
8
|
+
import { TriggerConfig as D, useResizeObserver as R } from "./hooks/use_resize_observer.js";
|
|
9
|
+
import { defaultValue as y } from "./default_value.js";
|
|
10
|
+
import { CalendarDatesGenerator as b } from "./calendar/calendar_dates_generator.js";
|
|
11
|
+
import { getDaysOfWeek as v } from "./calendar/get_days_of_week.js";
|
|
12
|
+
import { getMonthsOfYear as O } from "./calendar/get_months_of_year.js";
|
|
13
13
|
import { Month as A } from "./calendar/month.js";
|
|
14
14
|
import { ResponsiveRenderer as F } from "./responsive/responsive_renderer.js";
|
|
15
|
+
import { Draggable as L } from "./dnd/draggable/draggable.js";
|
|
16
|
+
import { DragHandle as z } from "./dnd/handle.js";
|
|
15
17
|
export {
|
|
16
|
-
|
|
18
|
+
b as CalendarDatesGenerator,
|
|
17
19
|
o as ClickAwayListener,
|
|
18
|
-
|
|
20
|
+
z as DragHandle,
|
|
21
|
+
L as Draggable,
|
|
22
|
+
a as FocusRedirect,
|
|
19
23
|
A as Month,
|
|
20
24
|
F as ResponsiveRenderer,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
p as ScrollAwayListener,
|
|
26
|
+
D as TriggerConfig,
|
|
27
|
+
y as defaultValue,
|
|
28
|
+
v as getDaysOfWeek,
|
|
29
|
+
O as getMonthsOfYear,
|
|
26
30
|
t as isEventWithinElement,
|
|
27
31
|
i as makeContextHook,
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
s as useDraggable,
|
|
33
|
+
l as useForkRef,
|
|
30
34
|
d as useMediaQuery,
|
|
31
|
-
|
|
35
|
+
R as useResizeObserver
|
|
32
36
|
};
|
|
33
37
|
//# sourceMappingURL=index.js.map
|
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tcn/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "TCN",
|
|
@@ -141,8 +141,8 @@
|
|
|
141
141
|
"@fontsource/lato": "^5.2.7",
|
|
142
142
|
"clsx": "^2.1.1",
|
|
143
143
|
"react-color": "^2.19.3",
|
|
144
|
-
"@tcn/
|
|
145
|
-
"@tcn/
|
|
144
|
+
"@tcn/icons": "2.2.1",
|
|
145
|
+
"@tcn/state": "1.2.0"
|
|
146
146
|
},
|
|
147
147
|
"scripts": {
|
|
148
148
|
"build": "vite build",
|
|
@@ -154,15 +154,15 @@
|
|
|
154
154
|
"start": "pnpm storybook",
|
|
155
155
|
"test": "vitest run",
|
|
156
156
|
"test-coverage": "vitest run --coverage",
|
|
157
|
-
"check-all": "concurrently 'pnpm check-types' 'pnpm
|
|
157
|
+
"check-all": "concurrently 'pnpm check-types' 'pnpm exec biome check .'",
|
|
158
158
|
"check-types": "tsc --project tsconfig.typecheck.json --noEmit",
|
|
159
|
-
"check-format": "pnpm
|
|
160
|
-
"check-lint": "pnpm
|
|
161
|
-
"check-imports": "pnpm
|
|
162
|
-
"fix-all": "pnpm
|
|
163
|
-
"fix-format": "pnpm
|
|
164
|
-
"fix-lint": "pnpm
|
|
165
|
-
"fix-imports": "pnpm
|
|
159
|
+
"check-format": "pnpm exec biome format .",
|
|
160
|
+
"check-lint": "pnpm exec biome lint .",
|
|
161
|
+
"check-imports": "pnpm exec biome check --formatter-enabled=false --linter-enabled=false --assist-enabled=true",
|
|
162
|
+
"fix-all": "pnpm exec biome check --write",
|
|
163
|
+
"fix-format": "pnpm exec biome format --write",
|
|
164
|
+
"fix-lint": "pnpm exec biome lint --write",
|
|
165
|
+
"fix-imports": "pnpm exec biome check --write --unsafe --formatter-enabled=false --linter-enabled=false --assist-enabled=true",
|
|
166
166
|
"publish-dry-run": "pnpm build && pnpm publish --dry-run --force --no-git-checks"
|
|
167
167
|
}
|
|
168
168
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ISubscription, Runner, Signal,
|
|
1
|
+
import { ISubscription, Runner, Signal, IWeakPromise } from '@tcn/state';
|
|
2
2
|
|
|
3
3
|
function fastCopy<T>(value: T): T {
|
|
4
4
|
return JSON.parse(JSON.stringify(value));
|
|
@@ -19,7 +19,7 @@ export interface FieldState<T> {
|
|
|
19
19
|
|
|
20
20
|
export interface FieldOptions<T> {
|
|
21
21
|
description?: string;
|
|
22
|
-
validate?: (value: T) => Promise<void> | void
|
|
22
|
+
validate?: (value: T) => Promise<void> | void | IWeakPromise<void>;
|
|
23
23
|
validateOnChange?: boolean;
|
|
24
24
|
equalityCheck?: (a: T, b: T) => boolean;
|
|
25
25
|
copyInitialValue?: (value: T) => T;
|
|
@@ -35,7 +35,7 @@ export class FieldPresenter<T> {
|
|
|
35
35
|
private _cloneInitialValue: (v: T) => T;
|
|
36
36
|
|
|
37
37
|
private _validationRunner: Runner<void>;
|
|
38
|
-
private _validate: (value: T) => Promise<void> | void |
|
|
38
|
+
private _validate: (value: T) => Promise<void> | void | IWeakPromise<void>;
|
|
39
39
|
private _subscriptions: ISubscription<any>[];
|
|
40
40
|
private _validateOnChange: boolean;
|
|
41
41
|
private _cancelToken = 'field_presenter_cancel';
|
|
@@ -57,6 +57,30 @@ export function PhoneNumberInput() {
|
|
|
57
57
|
<Base disabled />
|
|
58
58
|
</td>
|
|
59
59
|
</tr>
|
|
60
|
+
<tr>
|
|
61
|
+
<td>Disabled Phone Number</td>
|
|
62
|
+
<td>
|
|
63
|
+
<Base disabledPhoneNumber>
|
|
64
|
+
<Option value="+14355865953" label="John Doe" keywords={['john', 'doe']}>
|
|
65
|
+
John Doe - +1 (435) 586-5953
|
|
66
|
+
</Option>
|
|
67
|
+
<Option
|
|
68
|
+
value="+984355865954"
|
|
69
|
+
label="Jane Smith"
|
|
70
|
+
keywords={['jane', 'smith']}
|
|
71
|
+
>
|
|
72
|
+
Jane Smith - +98 (435) 586-5954
|
|
73
|
+
</Option>
|
|
74
|
+
<Option
|
|
75
|
+
value="+14355865955"
|
|
76
|
+
label="Bob Johnson"
|
|
77
|
+
keywords={['bob', 'johnson']}
|
|
78
|
+
>
|
|
79
|
+
Bob Johnson - +1 (435) 586-5955
|
|
80
|
+
</Option>
|
|
81
|
+
</Base>
|
|
82
|
+
</td>
|
|
83
|
+
</tr>
|
|
60
84
|
<tr>
|
|
61
85
|
<td>With Value</td>
|
|
62
86
|
<td>
|
|
@@ -130,6 +130,7 @@ export interface PhoneNumberInputProps
|
|
|
130
130
|
countrySelectRef?: React.Ref<HTMLButtonElement>;
|
|
131
131
|
phoneNumberInputRef?: React.Ref<HTMLInputElement>;
|
|
132
132
|
disabled?: boolean;
|
|
133
|
+
disabledPhoneNumber?: boolean;
|
|
133
134
|
allowedCountryCodes?: string[];
|
|
134
135
|
children?: React.ReactElement<OptionProps> | React.ReactElement<OptionProps>[];
|
|
135
136
|
}
|
|
@@ -144,6 +145,7 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
|
|
|
144
145
|
countrySelectRef: countryRef,
|
|
145
146
|
phoneNumberInputRef: numberRef,
|
|
146
147
|
disabled = false,
|
|
148
|
+
disabledPhoneNumber = false,
|
|
147
149
|
allowedCountryCodes,
|
|
148
150
|
children,
|
|
149
151
|
...props
|
|
@@ -341,8 +343,8 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
|
|
|
341
343
|
width="auto"
|
|
342
344
|
value={obfuscateValue ? '' : countryCode}
|
|
343
345
|
onChange={changeCountry}
|
|
344
|
-
disabled={disabled || obfuscateValue}
|
|
345
|
-
data-is-disabled={disabled || obfuscateValue}
|
|
346
|
+
disabled={disabled || obfuscateValue || disabledPhoneNumber}
|
|
347
|
+
data-is-disabled={disabled || obfuscateValue || disabledPhoneNumber}
|
|
346
348
|
data-is-obfuscated={obfuscateValue}
|
|
347
349
|
placeholder={obfuscateValue ? '––' : undefined}
|
|
348
350
|
>
|
|
@@ -364,8 +366,8 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
|
|
|
364
366
|
value=""
|
|
365
367
|
mask={createObfuscatedMasks(currentMasks)}
|
|
366
368
|
onChange={handleObfuscatedInputChange}
|
|
367
|
-
disabled={disabled}
|
|
368
|
-
data-is-disabled={disabled}
|
|
369
|
+
disabled={disabled || disabledPhoneNumber}
|
|
370
|
+
data-is-disabled={disabled || disabledPhoneNumber}
|
|
369
371
|
data-has-phone-book={showPhoneBook}
|
|
370
372
|
data-is-obfuscated={true}
|
|
371
373
|
className={clsx(
|
|
@@ -386,8 +388,8 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
|
|
|
386
388
|
value={phoneNumber}
|
|
387
389
|
mask={currentMasks}
|
|
388
390
|
onChange={transformValue}
|
|
389
|
-
disabled={disabled}
|
|
390
|
-
data-is-disabled={disabled}
|
|
391
|
+
disabled={disabled || disabledPhoneNumber}
|
|
392
|
+
data-is-disabled={disabled || disabledPhoneNumber}
|
|
391
393
|
data-has-phone-book={showPhoneBook}
|
|
392
394
|
data-is-obfuscated={false}
|
|
393
395
|
className={clsx(styles['phone-number-input'], 'tcn-phone-number-input')}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, type FC, type PropsWithChildren } from 'react';
|
|
1
|
+
import { forwardRef, useCallback, type FC, type PropsWithChildren } from 'react';
|
|
2
2
|
import { type BaseButtonProps } from '../../../actions/index.js';
|
|
3
3
|
import { HStack, type HStackProps } from '../../../stacks/h_stack.js';
|
|
4
4
|
import clsx from 'clsx';
|
|
@@ -6,15 +6,59 @@ import { Toggle } from '../../../actions/toggle/toggle.js';
|
|
|
6
6
|
|
|
7
7
|
export type TabsListProps = HStackProps;
|
|
8
8
|
|
|
9
|
+
const navigateTabs = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
10
|
+
const tabs = Array.from(event.currentTarget.querySelectorAll(':scope > [role="tab"]'));
|
|
11
|
+
const currentTab = event.currentTarget.querySelector(':focus');
|
|
12
|
+
const currentIndex = currentTab ? tabs.indexOf(currentTab) : -1;
|
|
13
|
+
if (currentIndex === -1) return; // Exit if the focused element is not a tab
|
|
14
|
+
let newIndex = 0;
|
|
15
|
+
|
|
16
|
+
switch (event.key) {
|
|
17
|
+
case 'ArrowRight':
|
|
18
|
+
newIndex = (currentIndex + 1) % tabs.length;
|
|
19
|
+
break;
|
|
20
|
+
case 'ArrowLeft':
|
|
21
|
+
newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
22
|
+
break;
|
|
23
|
+
case 'Home':
|
|
24
|
+
newIndex = 0;
|
|
25
|
+
break;
|
|
26
|
+
case 'End':
|
|
27
|
+
newIndex = tabs.length - 1;
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
return; // Exit if the key is not recognized
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
event.stopPropagation();
|
|
35
|
+
tabs[newIndex]['focus']();
|
|
36
|
+
};
|
|
37
|
+
|
|
9
38
|
export const TabsList: FC<PropsWithChildren<TabsListProps>> = ({
|
|
10
39
|
children,
|
|
11
40
|
className,
|
|
12
41
|
role = 'tablist',
|
|
13
42
|
as = 'menu',
|
|
43
|
+
onKeyDown,
|
|
14
44
|
...props
|
|
15
45
|
}) => {
|
|
46
|
+
const handleKeyDown = useCallback(
|
|
47
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
48
|
+
navigateTabs(event);
|
|
49
|
+
onKeyDown && onKeyDown(event);
|
|
50
|
+
},
|
|
51
|
+
[onKeyDown]
|
|
52
|
+
);
|
|
53
|
+
|
|
16
54
|
return (
|
|
17
|
-
<HStack
|
|
55
|
+
<HStack
|
|
56
|
+
onKeyDown={handleKeyDown}
|
|
57
|
+
as={as}
|
|
58
|
+
role={role}
|
|
59
|
+
className={clsx('tcn-tabs-list', className)}
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
18
62
|
{children}
|
|
19
63
|
</HStack>
|
|
20
64
|
);
|
|
@@ -13,7 +13,7 @@ export interface TabLinkProps
|
|
|
13
13
|
TabLinkOwnProps {}
|
|
14
14
|
|
|
15
15
|
export const TabLink = forwardRef<HTMLButtonElement, PropsWithChildren<TabLinkProps>>(
|
|
16
|
-
({ children, value, onClick, minWidth, maxWidth, ...props }, forwardedRef) => {
|
|
16
|
+
({ children, value, onClick, minWidth, maxWidth, id, ...props }, forwardedRef) => {
|
|
17
17
|
const { ref: internalRef, isMatch } = useTabLink(value);
|
|
18
18
|
const state = useTabs();
|
|
19
19
|
const ref = useForkRef(internalRef, forwardedRef);
|
|
@@ -36,6 +36,9 @@ export const TabLink = forwardRef<HTMLButtonElement, PropsWithChildren<TabLinkPr
|
|
|
36
36
|
onClick={handleClick}
|
|
37
37
|
minWidth={pickMinWidth}
|
|
38
38
|
maxWidth={pickMaxWidth}
|
|
39
|
+
id={`tab-${value}`}
|
|
40
|
+
aria-controls={`tabpanel-${value}`}
|
|
41
|
+
tabIndex={isMatch ? 0 : -1}
|
|
39
42
|
{...props}
|
|
40
43
|
>
|
|
41
44
|
{children}
|
|
@@ -3,15 +3,15 @@ import { useTabs } from '../context.js';
|
|
|
3
3
|
import { useTrackActiveItemRectangle } from '../../../../utils/css_utils.js';
|
|
4
4
|
|
|
5
5
|
export function useTabLink(value: string) {
|
|
6
|
-
const
|
|
7
|
-
const isMatch =
|
|
6
|
+
const { setActiveTrigger, value: currentValue } = useTabs();
|
|
7
|
+
const isMatch = currentValue === value;
|
|
8
8
|
const { ref, rectangle } = useTrackActiveItemRectangle(isMatch);
|
|
9
9
|
|
|
10
10
|
useLayoutEffect(() => {
|
|
11
11
|
if (rectangle) {
|
|
12
|
-
|
|
12
|
+
setActiveTrigger(rectangle);
|
|
13
13
|
}
|
|
14
|
-
}, [rectangle,
|
|
14
|
+
}, [rectangle, setActiveTrigger]);
|
|
15
15
|
|
|
16
16
|
return { ref, isMatch };
|
|
17
17
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import type { FC, PropsWithChildren } from 'react';
|
|
2
3
|
import { useTabs } from './context.js';
|
|
3
4
|
|
|
@@ -8,5 +9,14 @@ export interface TabProps {
|
|
|
8
9
|
export const Tab: FC<PropsWithChildren<TabProps>> = ({ value, children }) => {
|
|
9
10
|
const state = useTabs();
|
|
10
11
|
if (state.value !== value) return null;
|
|
12
|
+
|
|
13
|
+
if (children && React.isValidElement(children)) {
|
|
14
|
+
return React.cloneElement(children, {
|
|
15
|
+
...children.props,
|
|
16
|
+
id: children.props.id ?? `tabpanel-${value}`,
|
|
17
|
+
role: children.props.role ?? 'tabpanel',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
return children;
|
|
12
22
|
};
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import type { StoryObj } from '@storybook/react-vite';
|
|
2
2
|
import { Box } from '../../stacks/index.js';
|
|
3
3
|
import { VStack } from '../../stacks/v_stack.js';
|
|
4
|
-
import {
|
|
4
|
+
import { Caret } from './caret.js';
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
|
-
title: 'Overlays/
|
|
8
|
-
component:
|
|
7
|
+
title: 'Overlays/Caret',
|
|
8
|
+
component: Caret,
|
|
9
9
|
tags: ['autodocs'],
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
interface
|
|
12
|
+
interface CaretStoryProps {
|
|
13
13
|
direction: 'top' | 'bottom' | 'start' | 'end';
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const CaretStory = ({ direction }: CaretStoryProps) => {
|
|
17
17
|
return (
|
|
18
18
|
<VStack
|
|
19
19
|
minWidth="100px"
|
|
@@ -22,32 +22,32 @@ const CarrotStory = ({ direction }: CarrotStoryProps) => {
|
|
|
22
22
|
style={{ backgroundColor: 'gray' }}
|
|
23
23
|
>
|
|
24
24
|
<Box width="100px" height="100px" padding="24px" style={{ backgroundColor: 'red' }}>
|
|
25
|
-
<
|
|
25
|
+
<Caret direction={direction} />
|
|
26
26
|
</Box>
|
|
27
27
|
</VStack>
|
|
28
28
|
);
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
export const Top: StoryObj<
|
|
32
|
-
render: args => <
|
|
31
|
+
export const Top: StoryObj<CaretStoryProps> = {
|
|
32
|
+
render: args => <CaretStory {...args} />,
|
|
33
33
|
args: {
|
|
34
34
|
direction: 'top',
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
|
-
export const Bottom: StoryObj<
|
|
38
|
-
render: args => <
|
|
37
|
+
export const Bottom: StoryObj<CaretStoryProps> = {
|
|
38
|
+
render: args => <CaretStory {...args} />,
|
|
39
39
|
args: {
|
|
40
40
|
direction: 'bottom',
|
|
41
41
|
},
|
|
42
42
|
};
|
|
43
|
-
export const Start: StoryObj<
|
|
44
|
-
render: args => <
|
|
43
|
+
export const Start: StoryObj<CaretStoryProps> = {
|
|
44
|
+
render: args => <CaretStory {...args} />,
|
|
45
45
|
args: {
|
|
46
46
|
direction: 'start',
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
|
-
export const End: StoryObj<
|
|
50
|
-
render: args => <
|
|
49
|
+
export const End: StoryObj<CaretStoryProps> = {
|
|
50
|
+
render: args => <CaretStory {...args} />,
|
|
51
51
|
args: {
|
|
52
52
|
direction: 'end',
|
|
53
53
|
},
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { Box, type BoxProps } from '../../stacks/box/box.js';
|
|
4
|
+
|
|
5
|
+
export interface CaretOwnProps {
|
|
6
|
+
direction: 'top' | 'bottom' | 'start' | 'end';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CaretProps extends CaretOwnProps, BoxProps {}
|
|
10
|
+
|
|
11
|
+
export const Caret = forwardRef<HTMLElement, CaretProps>(function Caret(
|
|
12
|
+
{ direction, className, as = 'span', ...rest },
|
|
13
|
+
ref
|
|
14
|
+
) {
|
|
15
|
+
return (
|
|
16
|
+
<Box
|
|
17
|
+
ref={ref}
|
|
18
|
+
as="span"
|
|
19
|
+
data-direction={direction}
|
|
20
|
+
className={clsx('tcn-caret', className)}
|
|
21
|
+
{...rest}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
});
|
|
@@ -9,6 +9,7 @@ type TetheredStoryArgs = Pick<
|
|
|
9
9
|
| 'horizontalOrigin'
|
|
10
10
|
| 'verticalOffset'
|
|
11
11
|
| 'horizontalOffset'
|
|
12
|
+
| 'precision'
|
|
12
13
|
>;
|
|
13
14
|
|
|
14
15
|
export const tetheredArgTypes: ArgTypes<TetheredStoryArgs> = {
|
|
@@ -40,6 +41,12 @@ export const tetheredArgTypes: ArgTypes<TetheredStoryArgs> = {
|
|
|
40
41
|
control: { type: 'number' },
|
|
41
42
|
description: 'The offset to position the popper horizontally.',
|
|
42
43
|
},
|
|
44
|
+
precision: {
|
|
45
|
+
options: ['high', 'low'],
|
|
46
|
+
control: { type: 'radio' },
|
|
47
|
+
description:
|
|
48
|
+
'The precision level for positioning. High precision includes caret offset calculations.',
|
|
49
|
+
},
|
|
43
50
|
};
|
|
44
51
|
|
|
45
52
|
export const tetheredArgs: TetheredStoryArgs = {
|
|
@@ -49,4 +56,5 @@ export const tetheredArgs: TetheredStoryArgs = {
|
|
|
49
56
|
horizontalOrigin: 'center',
|
|
50
57
|
verticalOffset: 0,
|
|
51
58
|
horizontalOffset: 0,
|
|
59
|
+
precision: 'low',
|
|
52
60
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Position, Rectangle } from '../../../utils/index.js';
|
|
2
|
+
import type { HorizontalTether, VerticalTether } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export type CaretDirection = 'top' | 'bottom' | 'start' | 'end' | 'none';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculates the offset from the tethered element's top-left corner to the origin point
|
|
8
|
+
* based on tether dimensions and origin alignment.
|
|
9
|
+
*/
|
|
10
|
+
export function getOriginOffset(
|
|
11
|
+
tether: Rectangle,
|
|
12
|
+
hOrigin: HorizontalTether,
|
|
13
|
+
vOrigin: VerticalTether
|
|
14
|
+
): { yOffset: number; xOffset: number } {
|
|
15
|
+
let yOffset = 0;
|
|
16
|
+
let xOffset = 0;
|
|
17
|
+
|
|
18
|
+
// Calculate vertical offset
|
|
19
|
+
switch (vOrigin) {
|
|
20
|
+
case 'top':
|
|
21
|
+
yOffset = 0;
|
|
22
|
+
break;
|
|
23
|
+
case 'center':
|
|
24
|
+
yOffset = tether.dimensions.height / 2;
|
|
25
|
+
break;
|
|
26
|
+
case 'bottom':
|
|
27
|
+
yOffset = tether.dimensions.height;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Calculate horizontal offset
|
|
32
|
+
switch (hOrigin) {
|
|
33
|
+
case 'start':
|
|
34
|
+
xOffset = 0;
|
|
35
|
+
break;
|
|
36
|
+
case 'center':
|
|
37
|
+
xOffset = tether.dimensions.width / 2;
|
|
38
|
+
break;
|
|
39
|
+
case 'end':
|
|
40
|
+
xOffset = tether.dimensions.width;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { yOffset, xOffset };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Adds offset to baseline position to get absolute origin position.
|
|
49
|
+
*/
|
|
50
|
+
export function getOriginPosition(
|
|
51
|
+
baselinePosition: Position,
|
|
52
|
+
offset: { yOffset: number; xOffset: number }
|
|
53
|
+
): Position {
|
|
54
|
+
return {
|
|
55
|
+
x: baselinePosition.x + offset.xOffset,
|
|
56
|
+
y: baselinePosition.y + offset.yOffset,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Determines caret direction based on origin values.
|
|
62
|
+
*/
|
|
63
|
+
export function getOriginDirection(
|
|
64
|
+
vOrigin: VerticalTether,
|
|
65
|
+
hOrigin: HorizontalTether
|
|
66
|
+
): CaretDirection {
|
|
67
|
+
if (vOrigin !== 'center') {
|
|
68
|
+
return vOrigin;
|
|
69
|
+
} else if (hOrigin !== 'center') {
|
|
70
|
+
return hOrigin;
|
|
71
|
+
} else {
|
|
72
|
+
return 'none';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from 'react';
|
|
2
|
+
import type { CaretDirection } from './calculate_origin.js';
|
|
3
|
+
|
|
4
|
+
export function useCaretRefDimensions(
|
|
5
|
+
precision: 'high' | 'low',
|
|
6
|
+
originDirection: CaretDirection
|
|
7
|
+
): {
|
|
8
|
+
caretElementRef: (element: HTMLElement | null) => void;
|
|
9
|
+
caretSize: { width: number; height: number };
|
|
10
|
+
} {
|
|
11
|
+
const [caretElement, setCaretElement] = useState<HTMLElement | null>(null);
|
|
12
|
+
const [caretSize, setCaretSize] = useState({ width: 0, height: 0 });
|
|
13
|
+
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
if (precision === 'high' && originDirection !== 'none' && caretElement) {
|
|
16
|
+
const rect = caretElement.getBoundingClientRect();
|
|
17
|
+
setCaretSize({ width: rect.width, height: rect.height });
|
|
18
|
+
}
|
|
19
|
+
}, [precision, originDirection, caretElement]);
|
|
20
|
+
|
|
21
|
+
return { caretElementRef: setCaretElement, caretSize };
|
|
22
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
-
import type { HorizontalTether, VerticalTether } from '../types.js';
|
|
3
2
|
import { type Rectangle } from '../../../utils/index.js';
|
|
3
|
+
import type { HorizontalTether, VerticalTether } from '../types.js';
|
|
4
4
|
import { calculateTetheredPosition } from './calculate_position.js';
|
|
5
5
|
|
|
6
6
|
export interface UseTetherParams {
|
|
@@ -65,12 +65,13 @@ export function useTether({
|
|
|
65
65
|
horizontalOffset,
|
|
66
66
|
]);
|
|
67
67
|
|
|
68
|
-
// Update the position when the window is resized
|
|
69
68
|
useLayoutEffect(() => {
|
|
70
69
|
const update = () => {
|
|
71
70
|
const newPosition = getPosition();
|
|
72
71
|
if (!newPosition) return;
|
|
73
|
-
|
|
72
|
+
const hasChanged =
|
|
73
|
+
position.top !== newPosition.top || position.left !== newPosition.left;
|
|
74
|
+
if (hasChanged) {
|
|
74
75
|
setPosition(newPosition);
|
|
75
76
|
}
|
|
76
77
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from 'react';
|
|
2
|
+
import type { Rectangle } from '../../../utils/index.js';
|
|
3
|
+
|
|
4
|
+
function getTetherContentRect(element: HTMLElement | null): Rectangle {
|
|
5
|
+
if (!element) {
|
|
6
|
+
return {
|
|
7
|
+
dimensions: { width: 0, height: 0 },
|
|
8
|
+
position: { x: 0, y: 0 },
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const rect = element.getBoundingClientRect();
|
|
13
|
+
return {
|
|
14
|
+
dimensions: {
|
|
15
|
+
width: rect.width,
|
|
16
|
+
height: rect.height,
|
|
17
|
+
},
|
|
18
|
+
position: {
|
|
19
|
+
x: rect.left,
|
|
20
|
+
y: rect.top,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useTetherContentRect(ref: React.RefObject<HTMLElement>): Rectangle {
|
|
26
|
+
const [contentRect, setContentRect] = useState<Rectangle>(() =>
|
|
27
|
+
getTetherContentRect(ref.current)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
const update = () => {
|
|
32
|
+
setContentRect(getTetherContentRect(ref.current));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
update();
|
|
36
|
+
|
|
37
|
+
// Track resize events
|
|
38
|
+
window.addEventListener('resize', update);
|
|
39
|
+
// Track scroll events
|
|
40
|
+
window.addEventListener('scroll', update, true);
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
window.removeEventListener('resize', update);
|
|
44
|
+
window.removeEventListener('scroll', update, true);
|
|
45
|
+
};
|
|
46
|
+
}, [ref]);
|
|
47
|
+
|
|
48
|
+
return contentRect;
|
|
49
|
+
}
|