@wwog/react 1.3.14 → 1.4.1
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/README.md +56 -2
- package/dist/index.d.mts +163 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/Sundry/FocusTrap.test.tsx +238 -0
- package/src/components/Sundry/FocusTrap.tsx +238 -0
- package/src/components/Sundry/index.ts +1 -0
- package/src/utils/createExternalState.test.tsx +23 -0
- package/src/utils/createExternalState.ts +25 -19
- package/src/utils/focusable.test.ts +376 -0
- package/src/utils/focusable.ts +609 -0
- package/src/utils/index.ts +1 -0
package/README.md
CHANGED
|
@@ -465,6 +465,50 @@ function App() {
|
|
|
465
465
|
- `onError`: Optional callback for error reporting (e.g. logging to Sentry).
|
|
466
466
|
- `children`: Child elements to protect.
|
|
467
467
|
|
|
468
|
+
#### `<FocusTrap>` (v1.4.0+)
|
|
469
|
+
|
|
470
|
+
A focus trap component that constrains keyboard focus cycling to focusable elements within a container, with support for custom key mappings and navigation logic.
|
|
471
|
+
|
|
472
|
+
```tsx
|
|
473
|
+
import { FocusTrap } from "@wwog/react";
|
|
474
|
+
|
|
475
|
+
// Default Tab trapping
|
|
476
|
+
<FocusTrap>
|
|
477
|
+
<input />
|
|
478
|
+
<button>Save</button>
|
|
479
|
+
</FocusTrap>
|
|
480
|
+
|
|
481
|
+
// Arrow key navigation
|
|
482
|
+
<FocusTrap keyMap={{ ArrowDown: "next", ArrowUp: "prev" }}>
|
|
483
|
+
<div>
|
|
484
|
+
<button>Item 1</button>
|
|
485
|
+
<button>Item 2</button>
|
|
486
|
+
</div>
|
|
487
|
+
</FocusTrap>
|
|
488
|
+
|
|
489
|
+
// Cross-list navigation — items from multiple lists
|
|
490
|
+
// seamlessly cross in a single focus order
|
|
491
|
+
<FocusTrap keyMap={{ ArrowDown: "next", ArrowUp: "prev" }}>
|
|
492
|
+
<div>
|
|
493
|
+
<h4>List A</h4>
|
|
494
|
+
<button>A-1</button>
|
|
495
|
+
<button>A-2</button>
|
|
496
|
+
</div>
|
|
497
|
+
<div>
|
|
498
|
+
<h4>List B</h4>
|
|
499
|
+
<button>B-1</button>
|
|
500
|
+
<button>B-2</button>
|
|
501
|
+
</div>
|
|
502
|
+
</FocusTrap>
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
- `keyMap`: Custom key-to-direction mapping. Default `{ Tab: "next" }` (Shift+Tab auto-maps to `"prev"`). Example: `{ ArrowDown: "next", ArrowUp: "prev" }`.
|
|
506
|
+
- `onNavigate`: Custom focus resolution — return the element to focus, or `null` to use default cycling.
|
|
507
|
+
- `autoFocus`: Auto-focus the first tabbable element on mount.
|
|
508
|
+
- `restoreFocus`: Restore focus to the previously focused element on unmount.
|
|
509
|
+
- `disabled`: Temporarily disable the trap.
|
|
510
|
+
- `focusableOptions`: Options passed to `getTabbableElements` (e.g. `{ includeContainer: true }`).
|
|
511
|
+
|
|
468
512
|
#### `<SizeBox>`
|
|
469
513
|
|
|
470
514
|
Create a fixed-size container for layout adjustment and spacing control.
|
|
@@ -647,10 +691,20 @@ const result = ruleChecker(registrationData, rules);
|
|
|
647
691
|
|
|
648
692
|
#### `createExternalState` (v1.2.9+, useGetter added in v1.2.13)
|
|
649
693
|
|
|
694
|
+
> v1.3.14: Breaking: `use()` renamed to `useState()` for React 19 compiler compatibility (the compiler requires hook names to start with `use`; the old `use` method was misidentified as a non-hook)
|
|
650
695
|
> v1.2.21: Refactor the API to move sideeffects into options and enhance support for the transform interface
|
|
651
696
|
> v1.2.13: add useGetter
|
|
652
697
|
> Breaking: `sideEffect` replaced by `onSet` and `onChange` for clearer callback semantics
|
|
653
698
|
|
|
699
|
+
**Migration (v1.3.13 → v1.3.14)**
|
|
700
|
+
|
|
701
|
+
```diff
|
|
702
|
+
- const [theme, setTheme] = themeState.use();
|
|
703
|
+
+ const [theme, setTheme] = themeState.useState();
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
`useGetter()` is unchanged.
|
|
707
|
+
|
|
654
708
|
> A lightweight external state management utility that allows you to create and manage state outside the React component tree while maintaining perfect integration with components.
|
|
655
709
|
|
|
656
710
|
### `createStorageState` (v1.3.2+)
|
|
@@ -679,7 +733,7 @@ themeState.set("dark");
|
|
|
679
733
|
|
|
680
734
|
// Use the state in components
|
|
681
735
|
function ThemeConsumer() {
|
|
682
|
-
const [theme, setTheme] = themeState.
|
|
736
|
+
const [theme, setTheme] = themeState.useState();
|
|
683
737
|
|
|
684
738
|
return (
|
|
685
739
|
<div className={theme}>
|
|
@@ -708,7 +762,7 @@ function ReadOnlyThemeConsumer() {
|
|
|
708
762
|
- Returns an object with methods:
|
|
709
763
|
- `get()`: Get the current state value
|
|
710
764
|
- `set(newState)`: Update the state value
|
|
711
|
-
- `
|
|
765
|
+
- `useState()`: React Hook, returns `[state, setState]` for using this state in components (same return shape as React `useState`)
|
|
712
766
|
- `useGetter()`: React Hook that only returns the state value, useful when you only need to read the state
|
|
713
767
|
- `options.transform`: - `get` - `set`
|
|
714
768
|
|
package/dist/index.d.mts
CHANGED
|
@@ -558,6 +558,162 @@ interface BoundaryProps {
|
|
|
558
558
|
*/
|
|
559
559
|
declare function Boundary(props: BoundaryProps): ReactNode;
|
|
560
560
|
|
|
561
|
+
interface FocusableOptions {
|
|
562
|
+
/**
|
|
563
|
+
* @description_en Whether to include the container element in results.
|
|
564
|
+
* @description_zh 是否将容器元素本身纳入结果。
|
|
565
|
+
* @default false
|
|
566
|
+
*/
|
|
567
|
+
includeContainer?: boolean;
|
|
568
|
+
/**
|
|
569
|
+
* @description_en Traverse open shadow roots. Pass a function for custom shadow resolution.
|
|
570
|
+
* @description_zh 是否遍历 open shadow root;也可传入函数自定义 shadow 解析。
|
|
571
|
+
* @default true
|
|
572
|
+
*/
|
|
573
|
+
getShadowRoot?: boolean | GetShadowRootFn;
|
|
574
|
+
/**
|
|
575
|
+
* @description_en Strategy for visibility checks.
|
|
576
|
+
* @description_zh 可见性检查策略。
|
|
577
|
+
* @default 'full'
|
|
578
|
+
*/
|
|
579
|
+
displayCheck?: 'full' | 'full-native' | 'legacy-full' | 'non-zero-area' | 'none';
|
|
580
|
+
}
|
|
581
|
+
type GetShadowRootFn = (element: Element) => ShadowRoot | boolean | undefined;
|
|
582
|
+
/**
|
|
583
|
+
* @description_zh 获取元素的有效 tab 顺序值(含浏览器默认映射)。
|
|
584
|
+
* @description_en Returns the effective tab order for an element, including browser defaults.
|
|
585
|
+
*/
|
|
586
|
+
declare function getTabIndex(node: Element): number;
|
|
587
|
+
/**
|
|
588
|
+
* @description_zh 判断单个元素是否可被 programmatic focus(含 tabindex="-1")。
|
|
589
|
+
* @description_en Whether an element can receive programmatic focus (includes tabindex="-1").
|
|
590
|
+
*/
|
|
591
|
+
declare function isFocusable(node: Element, options?: FocusableOptions): boolean;
|
|
592
|
+
/**
|
|
593
|
+
* @description_zh 判断单个元素是否可通过 Tab 键聚焦。
|
|
594
|
+
* @description_en Whether an element can be focused via the Tab key.
|
|
595
|
+
*/
|
|
596
|
+
declare function isTabbable(node: Element, options?: FocusableOptions): boolean;
|
|
597
|
+
/**
|
|
598
|
+
* @description_zh 获取容器内所有可聚焦元素(含 tabindex="-1")。
|
|
599
|
+
* @description_en Returns all focusable elements within a container (includes tabindex="-1").
|
|
600
|
+
*/
|
|
601
|
+
declare function getFocusableElements(container: Element, options?: FocusableOptions): HTMLElement[];
|
|
602
|
+
/**
|
|
603
|
+
* @description_zh 获取容器内所有可通过 Tab 键循环聚焦的元素,按 tab 顺序排列。
|
|
604
|
+
* @description_en Returns tabbable elements within a container, sorted by tab order.
|
|
605
|
+
*/
|
|
606
|
+
declare function getTabbableElements(container: Element, options?: FocusableOptions): HTMLElement[];
|
|
607
|
+
|
|
608
|
+
type FocusDirection = 'next' | 'prev' | 'first' | 'last';
|
|
609
|
+
interface FocusTrapProps {
|
|
610
|
+
/**
|
|
611
|
+
* @description_en The child elements to trap focus within.
|
|
612
|
+
* @description_zh 需要劫持焦点的子元素。
|
|
613
|
+
*/
|
|
614
|
+
children?: ReactNode;
|
|
615
|
+
/**
|
|
616
|
+
* @description_en Whether to disable the focus trap.
|
|
617
|
+
* @description_zh 是否禁用焦点劫持。
|
|
618
|
+
* @default false
|
|
619
|
+
*/
|
|
620
|
+
disabled?: boolean;
|
|
621
|
+
/**
|
|
622
|
+
* @description_en Whether to auto-focus the first tabbable element on mount.
|
|
623
|
+
* @description_zh 是否在挂载时自动聚焦到第一个可 Tab 聚焦的元素。
|
|
624
|
+
* @default false
|
|
625
|
+
*/
|
|
626
|
+
autoFocus?: boolean;
|
|
627
|
+
/**
|
|
628
|
+
* @description_en Whether to restore focus to the previously focused element on unmount.
|
|
629
|
+
* @description_zh 是否在卸载时恢复焦点到之前聚焦的元素。
|
|
630
|
+
* @default false
|
|
631
|
+
*/
|
|
632
|
+
restoreFocus?: boolean;
|
|
633
|
+
/**
|
|
634
|
+
* @description_en Custom key-to-direction mapping to extend or override the default Tab-based navigation.
|
|
635
|
+
* @description_zh 自定义按键到焦点方向的映射,用于扩展或覆盖默认的 Tab 导航。
|
|
636
|
+
* @default { Tab: 'next' }
|
|
637
|
+
* @example
|
|
638
|
+
* ```tsx
|
|
639
|
+
* // Arrow up/down navigation
|
|
640
|
+
* keyMap={{ ArrowDown: 'next', ArrowUp: 'prev' }}
|
|
641
|
+
* // Arrow left/right navigation
|
|
642
|
+
* keyMap={{ ArrowRight: 'next', ArrowLeft: 'prev' }}
|
|
643
|
+
* ```
|
|
644
|
+
*/
|
|
645
|
+
keyMap?: Partial<Record<string, FocusDirection>>;
|
|
646
|
+
/**
|
|
647
|
+
* @description_en Custom focus resolution function. Return the element to focus, or null to use default cycle.
|
|
648
|
+
* @description_zh 自定义焦点解析函数。返回要聚焦的元素,或返回 null 使用默认循环行为。
|
|
649
|
+
* @optional
|
|
650
|
+
*/
|
|
651
|
+
onNavigate?: (current: HTMLElement | null, elements: HTMLElement[], direction: FocusDirection) => HTMLElement | null;
|
|
652
|
+
/**
|
|
653
|
+
* @description_en Options passed to getTabbableElements.
|
|
654
|
+
* @description_zh 传递给 getTabbableElements 的选项。
|
|
655
|
+
* @optional
|
|
656
|
+
*/
|
|
657
|
+
focusableOptions?: FocusableOptions;
|
|
658
|
+
/**
|
|
659
|
+
* @description_en CSS class name for the container.
|
|
660
|
+
* @description_zh 容器元素的 CSS 类名。
|
|
661
|
+
* @optional
|
|
662
|
+
*/
|
|
663
|
+
className?: string;
|
|
664
|
+
/**
|
|
665
|
+
* @description_en Inline styles for the container.
|
|
666
|
+
* @description_zh 容器元素的内联样式。
|
|
667
|
+
* @optional
|
|
668
|
+
*/
|
|
669
|
+
style?: React$1.CSSProperties;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* @description_zh 焦点陷阱组件,将键盘焦点循环限制在容器内的可聚焦元素中,支持自定义按键映射和导航逻辑。
|
|
673
|
+
* @description_en Focus trap component that constrains keyboard focus cycling to focusable elements within a container, with support for custom key mappings and navigation logic.
|
|
674
|
+
* @component
|
|
675
|
+
* @example
|
|
676
|
+
* ```tsx
|
|
677
|
+
* // Default Tab trapping
|
|
678
|
+
* <FocusTrap>
|
|
679
|
+
* <input />
|
|
680
|
+
* <button>Save</button>
|
|
681
|
+
* </FocusTrap>
|
|
682
|
+
*
|
|
683
|
+
* // Arrow key navigation
|
|
684
|
+
* <FocusTrap keyMap={{ ArrowDown: 'next', ArrowUp: 'prev' }}>
|
|
685
|
+
* <input />
|
|
686
|
+
* <button>Save</button>
|
|
687
|
+
* </FocusTrap>
|
|
688
|
+
*
|
|
689
|
+
* // With auto-focus and restore
|
|
690
|
+
* <FocusTrap autoFocus restoreFocus>
|
|
691
|
+
* <input />
|
|
692
|
+
* <button>Save</button>
|
|
693
|
+
* </FocusTrap>
|
|
694
|
+
*
|
|
695
|
+
* // Cross-list navigation: items from multiple lists are collected
|
|
696
|
+
* // into a single focus order, seamlessly crossing between lists.
|
|
697
|
+
* // ArrowDown from A-2 → B-1, ArrowUp from B-1 → A-2
|
|
698
|
+
* <FocusTrap keyMap={{ ArrowDown: 'next', ArrowUp: 'prev' }}>
|
|
699
|
+
* <div>
|
|
700
|
+
* <h3>List A</h3>
|
|
701
|
+
* <button>A-1</button>
|
|
702
|
+
* <button>A-2</button>
|
|
703
|
+
* </div>
|
|
704
|
+
* <div>
|
|
705
|
+
* <h3>List B</h3>
|
|
706
|
+
* <button>B-1</button>
|
|
707
|
+
* <button>B-2</button>
|
|
708
|
+
* </div>
|
|
709
|
+
* </FocusTrap>
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
declare function FocusTrap({ children, disabled, autoFocus, restoreFocus, keyMap, onNavigate, focusableOptions, className, style, }: FocusTrapProps): ReactNode;
|
|
713
|
+
declare namespace FocusTrap {
|
|
714
|
+
var displayName: string;
|
|
715
|
+
}
|
|
716
|
+
|
|
561
717
|
interface ArrayRenderProps<T> {
|
|
562
718
|
items: T[];
|
|
563
719
|
renderItem: (item: T, index: number) => React$1.ReactNode;
|
|
@@ -706,11 +862,11 @@ interface ExternalState<T, U = T> {
|
|
|
706
862
|
*/
|
|
707
863
|
set: (newState: U | ((prevState: U) => U)) => void;
|
|
708
864
|
/**
|
|
709
|
-
* @en React Hook for using external state in components
|
|
710
|
-
* @zh 在组件中使用外部状态的 React Hook
|
|
711
|
-
* @returns Array containing current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
865
|
+
* @en React Hook for using external state in components.
|
|
866
|
+
* @zh 在组件中使用外部状态的 React Hook。
|
|
867
|
+
* @returns Array containing current state and update function, similar to React useState / 包含当前状态和更新函数的数组,类似于 React useState
|
|
712
868
|
*/
|
|
713
|
-
|
|
869
|
+
useState: () => [U, (newState: U | ((prevState: U) => U)) => void];
|
|
714
870
|
/**
|
|
715
871
|
* @zh use的变体,只获取value.
|
|
716
872
|
* @en A variant of use that only gets the value.
|
|
@@ -739,7 +895,7 @@ interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
|
|
|
739
895
|
*
|
|
740
896
|
* // Use state in components
|
|
741
897
|
* function ThemeConsumer() {
|
|
742
|
-
* const [theme, setTheme] = themeState.
|
|
898
|
+
* const [theme, setTheme] = themeState.useState();
|
|
743
899
|
*
|
|
744
900
|
* return (
|
|
745
901
|
* <div className={theme}>
|
|
@@ -871,5 +1027,5 @@ declare function ruleChecker<T extends Record<string, unknown>, R extends RuleDe
|
|
|
871
1027
|
declare function getCurrentBreakpoint(breakpointDesc: BreakpointDesc, width: number): BreakpointName;
|
|
872
1028
|
declare function useScreen(breakpointDesc?: BreakpointDesc): "base" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
|
|
873
1029
|
|
|
874
|
-
export { ArrayRender, Boundary, Counter, DateRender, DefBreakpointDesc, False, If, Observer, Pipe, Portal, Repeat, Scope, SizeBox, Styles, Switch, Toggle, True, When, breakpoints, childrenLoop, createExternalState, createStorageState, cx, formatDate, getCurrentBreakpoint, ruleChecker, safePromiseTry, safePromiseWithResolvers, useControlled, useScreen };
|
|
875
|
-
export type { ApplyRules, ArrayRenderProps, ArrayRule, ArraySpecificProps, BaseRule, BooleanRule, BoundaryProps, BreakpointDesc, BreakpointName, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalState, ExternalStateCallback, ExternalStateOptions, ExternalWithKernel, FalseProps, FieldRule, IfProps, LengthRuleProps, NumberRangeProps, NumberRule, ObserverProps, PipeProps, PortalProps, RepeatProps, Responsive, RuleDescription, ScopeProps, StorageStateOptions, StringRule, StringSpecificProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
|
|
1030
|
+
export { ArrayRender, Boundary, Counter, DateRender, DefBreakpointDesc, False, FocusTrap, If, Observer, Pipe, Portal, Repeat, Scope, SizeBox, Styles, Switch, Toggle, True, When, breakpoints, childrenLoop, createExternalState, createStorageState, cx, formatDate, getCurrentBreakpoint, getFocusableElements, getTabIndex, getTabbableElements, isFocusable, isTabbable, ruleChecker, safePromiseTry, safePromiseWithResolvers, useControlled, useScreen };
|
|
1031
|
+
export type { ApplyRules, ArrayRenderProps, ArrayRule, ArraySpecificProps, BaseRule, BooleanRule, BoundaryProps, BreakpointDesc, BreakpointName, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalState, ExternalStateCallback, ExternalStateOptions, ExternalWithKernel, FalseProps, FieldRule, FocusDirection, FocusTrapProps, FocusableOptions, IfProps, LengthRuleProps, NumberRangeProps, NumberRule, ObserverProps, PipeProps, PortalProps, RepeatProps, Responsive, RuleDescription, ScopeProps, StorageStateOptions, StringRule, StringSpecificProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import u,{useMemo as S,Fragment as v,Children as q,isValidElement as U,cloneElement as G,useEffect as b,useState as A,useRef as N,Component as K,useCallback as Q,useSyncExternalStore as X}from"react";import{createPortal as ee}from"react-dom";function j(e,n){if(e===void 0)return;let t=0;if(Array.isArray(e)){for(const o of e)if(n(o,t++)===!1)break}else n(e,t)}const te=(e,n)=>e===n,x=e=>u.createElement(u.Fragment,null,e.children);x.displayName="Switch_Case";const C=e=>u.createElement(u.Fragment,null,e.children);C.displayName="Switch_Default";const F=e=>{const{value:n,compare:t=te,children:o,strict:r=!1}=e,l=new Set;let s=null,i=null,a=!1;return j(o,(f,m)=>{if(!u.isValidElement(f))throw new Error(`Switch Children only accepts valid React elements at index ${m}`);const c=f.type;if(c.displayName===x.displayName){const d=f.props;if(l.has(d.value))throw new Error(`Switch found duplicate Case value at index ${m}: ${JSON.stringify(d.value)}${r?" (detected in strict mode)":""}`);if(l.add(d.value),!s&&t(n,d.value)&&(s=d.children,r===!1))return!1}else if(c.displayName===C.displayName){if(a)throw new Error(`Switch can only have one Default child at index ${m}`);if(a=!0,i=f.props.children,!r&&s)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(c.displayName||c.name||c)} at index ${m}`)}),u.createElement(u.Fragment,null,s??i)};F.displayName="Switch",F.Case=x,F.Default=C,F.createTyped=function(){return{Switch:F,Case:x,Default:C}};const M=e=>u.createElement(u.Fragment,null,e.children),I=({children:e})=>u.createElement(u.Fragment,null,e),O=e=>u.createElement(u.Fragment,null,e.children);M.displayName="If_Then",I.displayName="If_Else",O.displayName="If_ElseIf";const w=({condition:e,children:n})=>{let t=null,o=null;const r=[];if(u.Children.forEach(n,l=>{if(!u.isValidElement(l))throw new Error("If component only accepts valid React elements");const s=l.type;if(s.displayName===M.displayName){if(t)throw new Error("If component can only have one Then child");t=l}else if(s.displayName===O.displayName)r.push(l);else if(s.displayName===I.displayName){if(o)throw new Error("If component can only have one Else child");o=l}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(s.displayName||s.name||s)}`)}),e)return t?u.createElement(u.Fragment,null,t.props.children):null;for(const l of r)if(l.props.condition)return u.createElement(u.Fragment,null,l.props.children);return o?u.createElement(u.Fragment,null,o.props.children):null};w.displayName="If",w.Then=M,w.ElseIf=O,w.Else=I,w.createTyped=function(){return{If:w,Then:M,ElseIf:O,Else:I}};const ne=({condition:e,children:n})=>e?u.createElement(u.Fragment,null,n):null,re=({condition:e,children:n})=>e===!1?u.createElement(u.Fragment,null,n):null,oe=({all:e,any:n,none:t,children:o,fallback:r})=>S(()=>(e&&(n||t)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(e&&e.length>0&&e.every(Boolean)||n&&n.length>0&&n.some(Boolean)||t&&t.length>0&&t.every(l=>!l))),[e,n,t])?u.createElement(u.Fragment,null,o):u.createElement(u.Fragment,null,r||null),le=({data:e,transform:n,render:t,fallback:o})=>{const r=S(()=>n.reduce((l,s)=>s(l),e),[e,n]);return r==null?u.createElement(u.Fragment,null,o||null):u.createElement(u.Fragment,null,t(r))},se=e=>{const{children:n,h:t,w:o,size:r,height:l,width:s,className:i}=e;return u.createElement("div",{style:{width:r||o||s,height:r||t||l,flexShrink:0},className:i},n)},ae=({let:e,props:n,children:t,fallback:o})=>{const r=S(()=>typeof e=="function"?e(n):e,[e,n]);return!t||!Object.keys(r).length?u.createElement(u.Fragment,null,o||null):u.createElement(u.Fragment,null,t(r))};function $(...e){const n=new Set;for(const t of e)if(t){if(typeof t=="string")n.add(t);else if(Array.isArray(t))t.forEach(o=>n.add(o));else if(typeof t=="object")for(const[o,r]of Object.entries(t))r&&n.add(o)}return Array.from(n).join(" ")}const ue=e=>typeof e=="object"&&!!e,P=({className:e,children:n,asWrapper:t=!1})=>{if(!n)return null;if(!e)return u.createElement(v,null,n);const o=typeof e=="string"?e:$(...Object.values(e));if(t)return u.createElement(t===!0?"div":t,{className:o},n);if(q.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),u.createElement(v,null,n);if(U(n)){const r=n;let l=r?.props?.className;return r?.type?.displayName===P.displayName&&ue(l)&&(l=$(...Object.values(l))),G(n,{className:$(o,l)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),u.createElement(v,null,n)};P.displayName="W/Styles";const ie=e=>{const{index:n=0,options:t,next:o,render:r}=e;b(()=>{if(t.length<n+1)throw new Error(`Index ${n} is out of bounds for options array of length ${t.length}. Defaulting to first option.`)},[n,t]);const[l,s]=A(n),i=()=>{s(a=>t.length?o?o(a,t):(a+1)%t.length:a)};return r(t[l],i)},ce=({onIntersect:e,threshold:n=.1,root:t=null,rootMargin:o="0px",triggerOnce:r=!1,disabled:l=!1,children:s,className:i,style:a})=>{const f=N(null),m=N(null),c=N(!1);return b(()=>{if(l||!f.current)return;if(!window.IntersectionObserver){console.warn("IntersectionObserver is not supported in this browser");return}const d=f.current,h=p=>{p.forEach(E=>{r&&c.current||(e(E,m.current),r&&(c.current=!0,m.current?.unobserve(d)))})};return m.current=new IntersectionObserver(h,{root:t,rootMargin:o,threshold:n}),m.current.observe(d),()=>{m.current&&m.current.disconnect()}},[e,n,t,o,r,l]),b(()=>{r||(c.current=!1)},[r]),u.createElement("div",{ref:f,className:i,style:a},s)};function fe({times:e,children:n}){if(e<=0)return null;const t=[];for(let o=0;o<e;o++)t.push(n(o));return u.createElement(v,null,t)}function de({to:e,children:n,disabled:t=!1}){const[o,r]=A(!1),l=N(e);if(l.current=e,b(()=>{r(!0)},[]),t)return u.createElement(u.Fragment,null,n);if(!o)return null;const s=l.current??document.body;return ee(n,s)}class me extends K{state={error:null};static getDerivedStateFromError(n){return{error:n}}componentDidCatch(n,t){this.props.onError?.(n,t)}reset=()=>{this.setState({error:null})};render(){return this.state.error?this.props.fallback(this.state.error,this.reset):this.props.children}}function he(e){return u.createElement(me,{...e})}function pe(e){const{items:n,renderItem:t,filter:o,renderEmpty:r,sort:l}=e;if(!n)return console.error("ArrayRender: items is null"),null;if(n.length===0)return r?r():null;if(l){let a=[...n];return o&&(a=a.filter(o)),a=a.sort(l),a.length===0?r?r():null:u.createElement(v,null,a.map((f,m)=>t(f,m)))}let s=0;const i=n.map((a,f)=>o&&!o(a)?(s++,null):t(a,f));return u.createElement(v,null,s===n.length?r?r():null:i)}function ge({source:e,format:n,children:t}){const o=S(()=>{if(e instanceof Date)return e;if(typeof e=="string"||typeof e=="number"){const l=new Date(e);return isNaN(l.getTime())?null:l}return null},[e]),r=S(()=>o?n?n(o):o.toLocaleString():null,[o,n]);return!r||!t?null:u.createElement(u.Fragment,null,t(r))}const ye="onChange",Ee="value";function Se(e){const{defaultValue:n,onBeforeChange:t,trigger:o=ye,valuePropName:r=Ee,props:l}=e,s=Object.prototype.hasOwnProperty.call(l,r),[i,a]=A(n),f=s?l[r]:i,m=S(()=>l[o],[l,o]),c=Q(d=>{const h=typeof d=="function"?d(f):d;t&&t(h,f)===!1||(s||a(h),m&&m(h))},[s,t,f,m]);return[f,c]}function we(e,...n){try{const t=e(...n);return t instanceof Promise?t:Promise.resolve(t)}catch(t){return Promise.reject(t)}}const J=typeof Promise.try=="function"?Promise.try.bind(Promise):we,ve=typeof Promise.withResolvers=="function"?Promise.withResolvers.bind(Promise):()=>{let e,n;return{promise:new Promise((t,o)=>{e=t,n=o}),resolve:e,reject:n}};function W(e,n={}){let t=typeof e=="function"?e():e;const o=[],{onSet:r,onChange:l,transform:s}=n,i=(c,d,h)=>{c&&J(c,d,h).catch(p=>{console.error("Error in external state callback, Please do it within side effects:",p)})},a=()=>{const c=t;return s?.get?s.get(c):c},f=c=>{const d=t,h=s?.get?s.get(d):d;t=s?.set?s.set(typeof c=="function"?c(h):c):typeof c=="function"?c(h):c,o.forEach(p=>p()),i(r,t,d),Object.is(t,d)||i(l,t,d)},m=()=>{const c=X(d=>(o.push(d),()=>{const h=o.indexOf(d);h>-1&&o.splice(h,1)}),()=>t,()=>t);return[s?.get?s.get(c):c,f]};return{get:a,set:f,use:m,useGetter:()=>{const[c]=m();return c},__listeners:o}}function Fe(e,n,t){const{storageType:o="local",onSet:r,onChange:l,transform:s}=t??{};let i=n;if(typeof window<"u"){const a=(o==="local"?localStorage:sessionStorage).getItem(e);if(a)try{i=JSON.parse(a)}catch(f){console.warn(`Failed to parse ${o}Storage value for key "${e}", using initial state:`,f),i=n}}return W(i,{onSet:(a,f)=>{typeof window<"u"&&(o==="local"?localStorage:sessionStorage).setItem(e,JSON.stringify(a)),r?.(a,f)},onChange:l,transform:s})}function be(e,n){const t=n||new Date,o=t.getFullYear(),r=t.getMonth()+1,l=t.getDate(),s=t.getHours(),i=t.getMinutes(),a=t.getSeconds(),f=t.getMilliseconds(),m=t.getDay(),c=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],d=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],h=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],p=["January","February","March","April","May","June","July","August","September","October","November","December"],E=d[m],R=c[m],Y=r-1,H=p[Y],z=h[Y],V={YY:o.toString().slice(2),YYYY:o.toString(),M:r.toString(),MM:r.toString().padStart(2,"0"),MMM:z,MMMM:H,D:l.toString(),DD:l.toString().padStart(2,"0"),d:m.toString(),dd:R,ddd:R,dddd:E,H:s.toString(),HH:s.toString().padStart(2,"0"),h:(s%12).toString(),hh:(s%12).toString().padStart(2,"0"),m:i.toString(),mm:i.toString().padStart(2,"0"),s:a.toString(),ss:a.toString().padStart(2,"0"),SSS:f.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:s<12?"AM":"PM",a:s<12?"am":"pm"};return e.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,Z=>V[Z])}class Ne{count=0;next(){return this.count++}}const D=["base","xs","sm","md","lg","xl","2xl","3xl"],L={xs:475,sm:640,md:768,lg:1024,xl:1280,"2xl":1536,"3xl":1920};function g(e,n,t){t&&(e[n]||(e[n]=[]),e[n].push(t))}function De(e){return e==null?!1:typeof e=="string"?e.trim().length>0:Array.isArray(e)?e.length>0:!0}function Ae(e){return e!=null}function y(e,n){return`${String(e)} ${n}`}const T={email:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,url:/^(https?:\/\/)?([\w.-]+)\.([a-z]{2,6})([/\w .-]*)*\/?$/,phone:/^1[3-9]\d{9}$/};function k(e,n,t,o,r){const l=De(n),s=Ae(n);if(t.required&&!l){g(r,e,t.message??y(e,"\u4E3A\u5FC5\u586B\u9879"));return}if(!(!s&&!t.required)){if(t.dependsOn){const i=t.dependsOn(o);i===!1?g(r,e,t.message??y(e,"\u4F9D\u8D56\u6761\u4EF6\u672A\u6EE1\u8DB3")):typeof i=="string"&&g(r,e,i)}if(typeof n=="string"){const i=t,{len:a,min:f,max:m,regex:c,email:d,url:h,phone:p}=i;typeof a=="number"&&n.length!==a&&g(r,e,t.message??y(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${a}`)),typeof f=="number"&&n.length<f&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u5C11\u4E8E ${f}`)),typeof m=="number"&&n.length>m&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${m}`)),c&&!c.test(n)&&g(r,e,t.message??y(e,"\u683C\u5F0F\u4E0D\u6B63\u786E")),d&&!T.email.test(n)&&g(r,e,t.message??y(e,"\u4E0D\u662F\u6709\u6548\u7684\u90AE\u7BB1")),h&&!T.url.test(n)&&g(r,e,t.message??y(e,"\u4E0D\u662F\u6709\u6548\u7684URL")),p&&!T.phone.test(n)&&g(r,e,t.message??y(e,"\u4E0D\u662F\u6709\u6548\u7684\u624B\u673A\u53F7"))}if(typeof n=="number"){const i=t,{min:a,max:f}=i;typeof a=="number"&&n<a&&g(r,e,t.message??y(e,`\u4E0D\u80FD\u5C0F\u4E8E ${a}`)),typeof f=="number"&&n>f&&g(r,e,t.message??y(e,`\u4E0D\u80FD\u5927\u4E8E ${f}`))}if(Array.isArray(n)){const i=t,{len:a,min:f,max:m,unique:c,elementRule:d}=i;typeof a=="number"&&n.length!==a&&g(r,e,t.message??y(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${a}`)),typeof f=="number"&&n.length<f&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u5C0F\u4E8E ${f}`)),typeof m=="number"&&n.length>m&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u5927\u4E8E ${m}`)),c&&new Set(n).size!==n.length&&g(r,e,t.message??y(e,"\u5143\u7D20\u5FC5\u987B\u552F\u4E00")),d&&n.forEach((h,p)=>{k(`${String(e)}[${p}]`,h,d,o,r)})}if(t.validator){const i=t.validator?.(n,o);i===!1?g(r,e,t.message??y(e,"\u6821\u9A8C\u672A\u901A\u8FC7")):typeof i=="string"&&g(r,e,i)}}}function xe(e,n){const t={};for(const r in n){const l=r,s=n[l];if(!s)continue;const i=e[l];if(Array.isArray(s))for(const a of s)k(l,i,a,e,t);else k(l,i,s,e,t)}const o=Object.values(t).reduce((r,l)=>(l&&r.push(...l),r),[]);return o.length>0?{valid:!1,errors:o,fieldErrors:t}:{valid:!0,data:e}}const Ce=[...D].reverse(),_=typeof window<"u";function B(e,n){for(const t of Ce){const o=e[t];if(o!==void 0&&!Number.isNaN(o)&&n>=o)return t}return"base"}function Me(e=L){const n=S(()=>JSON.stringify(e),[e]),t=N(e);t.current=e;const[o,r]=A(()=>_?B(e,window.innerWidth):"base");return b(()=>{if(!_)return;let l=[],s=[];const i=()=>{s.forEach(h=>h()),l=[],s=[];const a=t.current,f=B(a,window.innerWidth);r(f);const m=D.indexOf(f),c=D[m+1];if(c&&a[c]!==void 0){const h=a[c];if(Number.isNaN(h))throw new Error(`Invalid breakpoint value for ${c}: ${a[c]}`);{const p=window.matchMedia(`(min-width: ${h}px)`);l.push(p);const E=()=>i();p.addEventListener("change",E),s.push(()=>p.removeEventListener("change",E))}}const d=D[m-1];if(d&&a[d]!==void 0){const h=a[d];if(Number.isNaN(h))throw new Error(`Invalid breakpoint value for ${d}: ${a[d]}`);{const p=window.matchMedia(`(max-width: ${h-1}px)`);l.push(p);const E=()=>i();p.addEventListener("change",E),s.push(()=>p.removeEventListener("change",E))}}};return i(),()=>{s.forEach(a=>a())}},[n]),o}export{pe as ArrayRender,he as Boundary,Ne as Counter,ge as DateRender,L as DefBreakpointDesc,re as False,w as If,ce as Observer,le as Pipe,de as Portal,fe as Repeat,ae as Scope,se as SizeBox,P as Styles,F as Switch,ie as Toggle,ne as True,oe as When,D as breakpoints,j as childrenLoop,W as createExternalState,Fe as createStorageState,$ as cx,be as formatDate,B as getCurrentBreakpoint,xe as ruleChecker,J as safePromiseTry,ve as safePromiseWithResolvers,Se as useControlled,Me as useScreen};
|
|
1
|
+
import f,{useMemo as w,Fragment as D,Children as ge,isValidElement as ye,cloneElement as Ee,useEffect as b,useState as k,useRef as v,Component as Se,useCallback as I,useSyncExternalStore as be}from"react";import{createPortal as we}from"react-dom";function Q(e,t){if(e===void 0)return;let n=0;if(Array.isArray(e)){for(const r of e)if(t(r,n++)===!1)break}else t(e,n)}const ve=(e,t)=>e===t,M=e=>f.createElement(f.Fragment,null,e.children);M.displayName="Switch_Case";const $=e=>f.createElement(f.Fragment,null,e.children);$.displayName="Switch_Default";const x=e=>{const{value:t,compare:n=ve,children:r,strict:o=!1}=e,i=new Set;let a=null,u=null,s=!1;return Q(r,(l,h)=>{if(!f.isValidElement(l))throw new Error(`Switch Children only accepts valid React elements at index ${h}`);const c=l.type;if(c.displayName===M.displayName){const d=l.props;if(i.has(d.value))throw new Error(`Switch found duplicate Case value at index ${h}: ${JSON.stringify(d.value)}${o?" (detected in strict mode)":""}`);if(i.add(d.value),!a&&n(t,d.value)&&(a=d.children,o===!1))return!1}else if(c.displayName===$.displayName){if(s)throw new Error(`Switch can only have one Default child at index ${h}`);if(s=!0,u=l.props.children,!o&&a)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(c.displayName||c.name||c)} at index ${h}`)}),f.createElement(f.Fragment,null,a??u)};x.displayName="Switch",x.Case=M,x.Default=$,x.createTyped=function(){return{Switch:x,Case:M,Default:$}};const O=e=>f.createElement(f.Fragment,null,e.children),R=({children:e})=>f.createElement(f.Fragment,null,e),P=e=>f.createElement(f.Fragment,null,e.children);O.displayName="If_Then",R.displayName="If_Else",P.displayName="If_ElseIf";const N=({condition:e,children:t})=>{let n=null,r=null;const o=[];if(f.Children.forEach(t,i=>{if(!f.isValidElement(i))throw new Error("If component only accepts valid React elements");const a=i.type;if(a.displayName===O.displayName){if(n)throw new Error("If component can only have one Then child");n=i}else if(a.displayName===P.displayName)o.push(i);else if(a.displayName===R.displayName){if(r)throw new Error("If component can only have one Else child");r=i}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(a.displayName||a.name||a)}`)}),e)return n?f.createElement(f.Fragment,null,n.props.children):null;for(const i of o)if(i.props.condition)return f.createElement(f.Fragment,null,i.props.children);return r?f.createElement(f.Fragment,null,r.props.children):null};N.displayName="If",N.Then=O,N.ElseIf=P,N.Else=R,N.createTyped=function(){return{If:N,Then:O,ElseIf:P,Else:R}};const Ne=({condition:e,children:t})=>e?f.createElement(f.Fragment,null,t):null,Fe=({condition:e,children:t})=>e===!1?f.createElement(f.Fragment,null,t):null,Ce=({all:e,any:t,none:n,children:r,fallback:o})=>w(()=>(e&&(t||n)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(e&&e.length>0&&e.every(Boolean)||t&&t.length>0&&t.some(Boolean)||n&&n.length>0&&n.every(i=>!i))),[e,t,n])?f.createElement(f.Fragment,null,r):f.createElement(f.Fragment,null,o||null),Ae=({data:e,transform:t,render:n,fallback:r})=>{const o=w(()=>t.reduce((i,a)=>a(i),e),[e,t]);return o==null?f.createElement(f.Fragment,null,r||null):f.createElement(f.Fragment,null,n(o))},De=e=>{const{children:t,h:n,w:r,size:o,height:i,width:a,className:u}=e;return f.createElement("div",{style:{width:o||r||a,height:o||n||i,flexShrink:0},className:u},t)},xe=({let:e,props:t,children:n,fallback:r})=>{const o=w(()=>typeof e=="function"?e(t):e,[e,t]);return!n||!Object.keys(o).length?f.createElement(f.Fragment,null,r||null):f.createElement(f.Fragment,null,n(o))};function B(...e){const t=new Set;for(const n of e)if(n){if(typeof n=="string")t.add(n);else if(Array.isArray(n))n.forEach(r=>t.add(r));else if(typeof n=="object")for(const[r,o]of Object.entries(n))o&&t.add(r)}return Array.from(t).join(" ")}const Te=e=>typeof e=="object"&&!!e,V=({className:e,children:t,asWrapper:n=!1})=>{if(!t)return null;if(!e)return f.createElement(D,null,t);const r=typeof e=="string"?e:B(...Object.values(e));if(n)return f.createElement(n===!0?"div":n,{className:r},t);if(ge.count(t)>1)return console.error("<Styles>: children has more than one child. Please check your code."),f.createElement(D,null,t);if(ye(t)){const o=t;let i=o?.props?.className;return o?.type?.displayName===V.displayName&&Te(i)&&(i=B(...Object.values(i))),Ee(t,{className:B(r,i)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),f.createElement(D,null,t)};V.displayName="W/Styles";const ke=e=>{const{index:t=0,options:n,next:r,render:o}=e;b(()=>{if(n.length<t+1)throw new Error(`Index ${t} is out of bounds for options array of length ${n.length}. Defaulting to first option.`)},[t,n]);const[i,a]=k(t),u=()=>{a(s=>n.length?r?r(s,n):(s+1)%n.length:s)};return o(n[i],u)},Ie=({onIntersect:e,threshold:t=.1,root:n=null,rootMargin:r="0px",triggerOnce:o=!1,disabled:i=!1,children:a,className:u,style:s})=>{const l=v(null),h=v(null),c=v(!1);return b(()=>{if(i||!l.current)return;if(!window.IntersectionObserver){console.warn("IntersectionObserver is not supported in this browser");return}const d=l.current,p=m=>{m.forEach(g=>{o&&c.current||(e(g,h.current),o&&(c.current=!0,h.current?.unobserve(d)))})};return h.current=new IntersectionObserver(p,{root:n,rootMargin:r,threshold:t}),h.current.observe(d),()=>{h.current&&h.current.disconnect()}},[e,t,n,r,o,i]),b(()=>{o||(c.current=!1)},[o]),f.createElement("div",{ref:l,className:u,style:s},a)};function Me({times:e,children:t}){if(e<=0)return null;const n=[];for(let r=0;r<e;r++)n.push(t(r));return f.createElement(D,null,n)}function $e({to:e,children:t,disabled:n=!1}){const[r,o]=k(!1),i=v(e);if(i.current=e,b(()=>{o(!0)},[]),n)return f.createElement(f.Fragment,null,t);if(!r)return null;const a=i.current??document.body;return we(t,a)}class Oe extends Se{state={error:null};static getDerivedStateFromError(t){return{error:t}}componentDidCatch(t,n){this.props.onError?.(t,n)}reset=()=>{this.setState({error:null})};render(){return this.state.error?this.props.fallback(this.state.error,this.reset):this.props.children}}function Re(e){return f.createElement(Oe,{...e})}const ee=["input:not([inert]):not([inert] *)","select:not([inert]):not([inert] *)","textarea:not([inert]):not([inert] *)","a[href]:not([inert]):not([inert] *)","area[href]:not([inert]):not([inert] *)","button:not([inert]):not([inert] *)","[tabindex]:not(slot):not([inert]):not([inert] *)","audio[controls]:not([inert]):not([inert] *)","video[controls]:not([inert]):not([inert] *)",'[contenteditable]:not([contenteditable="false"]):not([inert]):not([inert] *)',"details>summary:first-of-type:not([inert]):not([inert] *)","details:not([inert]):not([inert] *)"],U=ee.join(","),te=[...ee,"iframe:not([inert]):not([inert] *)"].join(","),ne=typeof Element>"u"?()=>!1:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector;function F(e,t){return ne?ne.call(e,t):!1}const _={includeContainer:!1,getShadowRoot:!0,displayCheck:"full"};function L(e){return{includeContainer:e?.includeContainer??_.includeContainer,getShadowRoot:e?.getShadowRoot??_.getShadowRoot,displayCheck:e?.displayCheck??_.displayCheck}}function j(e){return e.getRootNode()}function Y(e,t=!0){if(!(e instanceof Element))return!1;const n=e.getAttribute("inert");return n===""||n==="true"?!0:t?typeof e.closest=="function"?e.closest("[inert]")!==null:Y(e.parentElement,!0):!1}function Pe(e){const t=e.getAttribute("contenteditable");return t===""||t==="true"}function re(e){return!Number.isNaN(Number.parseInt(e.getAttribute("tabindex")??"",10))}function q(e){return e instanceof HTMLElement?e.tabIndex<0&&(/^(AUDIO|VIDEO|DETAILS)$/i.test(e.tagName)||Pe(e))&&!re(e)?0:e.tabIndex:-1}function Be(e,t){const n=q(e);return n<0&&t&&!re(e)?0:n}function oe(e){return e.tagName==="INPUT"}function Le(e){return oe(e)&&e.type==="hidden"}function je(e){return e.tagName==="DETAILS"&&Array.prototype.some.call(e.children,t=>t.tagName==="SUMMARY")}function Ye(e,t){for(const n of e)if(n.checked&&n.form===t)return n}function We(e){if(!e.name)return!0;const t=e.form??j(e),n=(o=>t.querySelectorAll(`input[type="radio"][name="${CSS.escape(o)}"]`))(e.name),r=Ye(Array.from(n),e.form);return!r||r===e}function He(e){return oe(e)&&e.type==="radio"&&!We(e)}function Je(e){let t=j(e),n=t instanceof ShadowRoot?t.host:void 0,r=!1;if(t&&t!==e)for(r=!!(n?.ownerDocument?.contains(n)||e.ownerDocument?.contains(e));!r&&n;)t=j(n),n=t instanceof ShadowRoot?t.host:void 0,r=!!n?.ownerDocument?.contains(n);return r}function ie(e){const{width:t,height:n}=e.getBoundingClientRect();return t===0&&n===0}function Ve(e,t){if(t.displayCheck==="none")return!1;if(t.displayCheck==="full-native"&&"checkVisibility"in e)return!e.checkVisibility({checkOpacity:!1,opacityProperty:!1,contentVisibilityAuto:!0,visibilityProperty:!0,checkVisibilityCSS:!0});const{visibility:n}=getComputedStyle(e);if(n==="hidden"||n==="collapse")return!0;const r=F(e,"details>summary:first-of-type")?e.parentElement:e;if(r&&F(r,"details:not([open]) *"))return!0;if(t.displayCheck==="full"||t.displayCheck==="full-native"||t.displayCheck==="legacy-full"){if(typeof t.getShadowRoot=="function"){const o=e;let i=e;for(;i;){const a=i.parentElement,u=j(i);if(a&&!a.shadowRoot&&t.getShadowRoot(a)===!0)return ie(i);i.assignedSlot?i=i.assignedSlot:!a&&u!==i.ownerDocument?i=u instanceof ShadowRoot?u.host:null:i=a}e=o}if(Je(e))return e.getClientRects().length===0;if(t.displayCheck!=="legacy-full")return!0}else if(t.displayCheck==="non-zero-area")return ie(e);return!1}function Ue(e){if(!/^(INPUT|BUTTON|SELECT|TEXTAREA)$/i.test(e.tagName))return!1;let t=e.parentElement;for(;t;){if(t.tagName==="FIELDSET"&&t.disabled){for(let n=0;n<t.children.length;n++){const r=t.children.item(n);if(r?.tagName==="LEGEND")return F(t,"fieldset[disabled] *")?!0:!r.contains(e)}return!0}t=t.parentElement}return!1}function _e(e){return"disabled"in e&&!!e.disabled}function z(e,t){return!(!(e instanceof HTMLElement)||_e(e)||Le(e)||Ve(e,t)||je(e)||Ue(e))}function Z(e,t){return He(e)||q(e)<0?!1:z(e,t)}function qe(e){const t=Number.parseInt(e.getAttribute("tabindex")??"",10);return!!(Number.isNaN(t)||t>=0)}function ae(e,t,n,r=U){if(Y(e))return[];const o=Array.from(e.querySelectorAll(r));return t&&F(e,r)&&o.unshift(e),o.filter(n)}function W(e,t,n,r,o,i,a){const u=[],s=[...e];for(;s.length>0;){const l=s.shift();if(!l||Y(l,!1))continue;if(l.tagName==="SLOT"){const d=l.assignedElements(),p=d.length>0?d:Array.from(l.children),m=W(p,!0,n,r,o,i,a);o?u.push(...m):u.push({scopeParent:l,candidates:m});continue}F(l,i)&&r(l)&&(t||!e.includes(l))&&u.push(l);const h=n.getShadowRoot,c=l.shadowRoot??(typeof h=="function"?h(l):void 0);if(c&&!Y(c,!1)&&(!a||a(l))){const d=W(c===!0?Array.from(l.children):Array.from(c.children),!0,n,r,o,i,a);o?u.push(...d):u.push({scopeParent:l,candidates:d})}else s.unshift(...Array.from(l.children))}return u}function ze(e,t){return e.tabIndex===t.tabIndex?e.documentOrder-t.documentOrder:e.tabIndex-t.tabIndex}function se(e){const t=[],n=[];return e.forEach((r,o)=>{const i="scopeParent"in r,a=i?r.scopeParent:r,u=Be(a,i),s=i?se(r.candidates):[a];u===0?t.push(...s):n.push({documentOrder:o,tabIndex:u,item:r,isScope:i,content:s})}),n.sort(ze).flatMap(r=>r.content).concat(t)}function Ze(e,t,n,r,o,i){return t.getShadowRoot?W([e],t.includeContainer,t,n,r,o,i):ae(e,t.includeContainer,n,o)}function le(e){return e.filter(t=>t instanceof HTMLElement)}function Ge(e,t){const n=L(t);return F(e,te)?z(e,n):!1}function Xe(e,t){const n=L(t);return F(e,U)?Z(e,n):!1}function Ke(e,t){const n=L(t),r=Ze(e,n,o=>z(o,n),!0,te);return le(r)}function ue(e,t){const n=L(t);let r;return n.getShadowRoot?r=W([e],n.includeContainer,n,o=>Z(o,n),!1,U,qe):r=ae(e,n.includeContainer,o=>Z(o,n)),le(se(r))}const Qe={Tab:"next"};function et(e,t,n){const r=t.length-1;switch(n){case"next":return e<r?e+1:0;case"prev":return e>0?e-1:r;case"first":return 0;case"last":return r}}function ce({children:e,disabled:t=!1,autoFocus:n=!1,restoreFocus:r=!1,keyMap:o,onNavigate:i,focusableOptions:a,className:u,style:s}){const l=v(null),h=v(null),c=I(()=>l.current?ue(l.current,a):[],[a]),d=I(m=>{m&&typeof m.focus=="function"&&m.focus()},[]),p=I(m=>{if(t)return;let g={...Qe,...o}[m.key];if(!g)return;m.preventDefault(),m.stopPropagation();const S=c();if(S.length===0)return;m.key==="Tab"&&m.shiftKey&&(g="prev");const C=document.activeElement,H=C?S.indexOf(C):-1;let A=null;if(i&&(A=i(C,S,g)),!A){const J=et(H,S,g);A=S[J]}d(A)},[t,o,i,c,d]);return b(()=>{if(t)return;const m=l.current;if(m)return m.addEventListener("keydown",p),()=>{m.removeEventListener("keydown",p)}},[t,p]),b(()=>{if(t||!n)return;const m=c();m.length>0&&m[0].focus()},[t,n,c]),b(()=>{if(t||!r)return;const m=document.activeElement;return m&&m!==l.current&&(h.current=m),()=>{const g=h.current;g&&typeof g.focus=="function"&&g.focus()}},[t,r]),f.createElement("div",{ref:l,className:u,style:s},e)}ce.displayName="W/FocusTrap";function tt(e){const{items:t,renderItem:n,filter:r,renderEmpty:o,sort:i}=e;if(!t)return console.error("ArrayRender: items is null"),null;if(t.length===0)return o?o():null;if(i){let s=[...t];return r&&(s=s.filter(r)),s=s.sort(i),s.length===0?o?o():null:f.createElement(D,null,s.map((l,h)=>n(l,h)))}let a=0;const u=t.map((s,l)=>r&&!r(s)?(a++,null):n(s,l));return f.createElement(D,null,a===t.length?o?o():null:u)}function nt({source:e,format:t,children:n}){const r=w(()=>{if(e instanceof Date)return e;if(typeof e=="string"||typeof e=="number"){const i=new Date(e);return isNaN(i.getTime())?null:i}return null},[e]),o=w(()=>r?t?t(r):r.toLocaleString():null,[r,t]);return!o||!n?null:f.createElement(f.Fragment,null,n(o))}const rt="onChange",ot="value";function it(e){const{defaultValue:t,onBeforeChange:n,trigger:r=rt,valuePropName:o=ot,props:i}=e,a=Object.prototype.hasOwnProperty.call(i,o),[u,s]=k(t),l=a?i[o]:u,h=w(()=>i[r],[i,r]),c=I(d=>{const p=typeof d=="function"?d(l):d;n&&n(p,l)===!1||(a||s(p),h&&h(p))},[a,n,l,h]);return[l,c]}function at(e,...t){try{const n=e(...t);return n instanceof Promise?n:Promise.resolve(n)}catch(n){return Promise.reject(n)}}const fe=typeof Promise.try=="function"?Promise.try.bind(Promise):at,st=typeof Promise.withResolvers=="function"?Promise.withResolvers.bind(Promise):()=>{let e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}};function de(e,t={}){let n=typeof e=="function"?e():e;const r=[],{onSet:o,onChange:i,transform:a}=t,u=(c,d,p)=>{c&&fe(c,d,p).catch(m=>{console.error("Error in external state callback, Please do it within side effects:",m)})},s=()=>{const c=n;return a?.get?a.get(c):c},l=c=>{const d=n,p=a?.get?a.get(d):d;n=a?.set?a.set(typeof c=="function"?c(p):c):typeof c=="function"?c(p):c,r.forEach(m=>m()),u(o,n,d),Object.is(n,d)||u(i,n,d)},h=()=>{const c=be(d=>(r.push(d),()=>{const p=r.indexOf(d);p>-1&&r.splice(p,1)}),()=>n,()=>n);return[a?.get?a.get(c):c,l]};return{get:s,set:l,useState:h,useGetter:()=>{const[c]=h();return c},__listeners:r}}function lt(e,t,n){const{storageType:r="local",onSet:o,onChange:i,transform:a}=n??{};let u=t;if(typeof window<"u"){const s=(r==="local"?localStorage:sessionStorage).getItem(e);if(s)try{u=JSON.parse(s)}catch(l){console.warn(`Failed to parse ${r}Storage value for key "${e}", using initial state:`,l),u=t}}return de(u,{onSet:(s,l)=>{typeof window<"u"&&(r==="local"?localStorage:sessionStorage).setItem(e,JSON.stringify(s)),o?.(s,l)},onChange:i,transform:a})}function ut(e,t){const n=t||new Date,r=n.getFullYear(),o=n.getMonth()+1,i=n.getDate(),a=n.getHours(),u=n.getMinutes(),s=n.getSeconds(),l=n.getMilliseconds(),h=n.getDay(),c=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],d=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],p=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],m=["January","February","March","April","May","June","July","August","September","October","November","December"],g=d[h],S=c[h],C=o-1,H=m[C],A=p[C],J={YY:r.toString().slice(2),YYYY:r.toString(),M:o.toString(),MM:o.toString().padStart(2,"0"),MMM:A,MMMM:H,D:i.toString(),DD:i.toString().padStart(2,"0"),d:h.toString(),dd:S,ddd:S,dddd:g,H:a.toString(),HH:a.toString().padStart(2,"0"),h:(a%12).toString(),hh:(a%12).toString().padStart(2,"0"),m:u.toString(),mm:u.toString().padStart(2,"0"),s:s.toString(),ss:s.toString().padStart(2,"0"),SSS:l.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:a<12?"AM":"PM",a:a<12?"am":"pm"};return e.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,pe=>J[pe])}class ct{count=0;next(){return this.count++}}const T=["base","xs","sm","md","lg","xl","2xl","3xl"],me={xs:475,sm:640,md:768,lg:1024,xl:1280,"2xl":1536,"3xl":1920};function y(e,t,n){n&&(e[t]||(e[t]=[]),e[t].push(n))}function ft(e){return e==null?!1:typeof e=="string"?e.trim().length>0:Array.isArray(e)?e.length>0:!0}function dt(e){return e!=null}function E(e,t){return`${String(e)} ${t}`}const G={email:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,url:/^(https?:\/\/)?([\w.-]+)\.([a-z]{2,6})([/\w .-]*)*\/?$/,phone:/^1[3-9]\d{9}$/};function X(e,t,n,r,o){const i=ft(t),a=dt(t);if(n.required&&!i){y(o,e,n.message??E(e,"\u4E3A\u5FC5\u586B\u9879"));return}if(!(!a&&!n.required)){if(n.dependsOn){const u=n.dependsOn(r);u===!1?y(o,e,n.message??E(e,"\u4F9D\u8D56\u6761\u4EF6\u672A\u6EE1\u8DB3")):typeof u=="string"&&y(o,e,u)}if(typeof t=="string"){const u=n,{len:s,min:l,max:h,regex:c,email:d,url:p,phone:m}=u;typeof s=="number"&&t.length!==s&&y(o,e,n.message??E(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${s}`)),typeof l=="number"&&t.length<l&&y(o,e,n.message??E(e,`\u957F\u5EA6\u4E0D\u80FD\u5C11\u4E8E ${l}`)),typeof h=="number"&&t.length>h&&y(o,e,n.message??E(e,`\u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${h}`)),c&&!c.test(t)&&y(o,e,n.message??E(e,"\u683C\u5F0F\u4E0D\u6B63\u786E")),d&&!G.email.test(t)&&y(o,e,n.message??E(e,"\u4E0D\u662F\u6709\u6548\u7684\u90AE\u7BB1")),p&&!G.url.test(t)&&y(o,e,n.message??E(e,"\u4E0D\u662F\u6709\u6548\u7684URL")),m&&!G.phone.test(t)&&y(o,e,n.message??E(e,"\u4E0D\u662F\u6709\u6548\u7684\u624B\u673A\u53F7"))}if(typeof t=="number"){const u=n,{min:s,max:l}=u;typeof s=="number"&&t<s&&y(o,e,n.message??E(e,`\u4E0D\u80FD\u5C0F\u4E8E ${s}`)),typeof l=="number"&&t>l&&y(o,e,n.message??E(e,`\u4E0D\u80FD\u5927\u4E8E ${l}`))}if(Array.isArray(t)){const u=n,{len:s,min:l,max:h,unique:c,elementRule:d}=u;typeof s=="number"&&t.length!==s&&y(o,e,n.message??E(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${s}`)),typeof l=="number"&&t.length<l&&y(o,e,n.message??E(e,`\u957F\u5EA6\u4E0D\u80FD\u5C0F\u4E8E ${l}`)),typeof h=="number"&&t.length>h&&y(o,e,n.message??E(e,`\u957F\u5EA6\u4E0D\u80FD\u5927\u4E8E ${h}`)),c&&new Set(t).size!==t.length&&y(o,e,n.message??E(e,"\u5143\u7D20\u5FC5\u987B\u552F\u4E00")),d&&t.forEach((p,m)=>{X(`${String(e)}[${m}]`,p,d,r,o)})}if(n.validator){const u=n.validator?.(t,r);u===!1?y(o,e,n.message??E(e,"\u6821\u9A8C\u672A\u901A\u8FC7")):typeof u=="string"&&y(o,e,u)}}}function mt(e,t){const n={};for(const o in t){const i=o,a=t[i];if(!a)continue;const u=e[i];if(Array.isArray(a))for(const s of a)X(i,u,s,e,n);else X(i,u,a,e,n)}const r=Object.values(n).reduce((o,i)=>(i&&o.push(...i),o),[]);return r.length>0?{valid:!1,errors:r,fieldErrors:n}:{valid:!0,data:e}}const ht=[...T].reverse(),he=typeof window<"u";function K(e,t){for(const n of ht){const r=e[n];if(r!==void 0&&!Number.isNaN(r)&&t>=r)return n}return"base"}function pt(e=me){const t=w(()=>JSON.stringify(e),[e]),n=v(e);n.current=e;const[r,o]=k(()=>he?K(e,window.innerWidth):"base");return b(()=>{if(!he)return;let i=[],a=[];const u=()=>{a.forEach(p=>p()),i=[],a=[];const s=n.current,l=K(s,window.innerWidth);o(l);const h=T.indexOf(l),c=T[h+1];if(c&&s[c]!==void 0){const p=s[c];if(Number.isNaN(p))throw new Error(`Invalid breakpoint value for ${c}: ${s[c]}`);{const m=window.matchMedia(`(min-width: ${p}px)`);i.push(m);const g=()=>u();m.addEventListener("change",g),a.push(()=>m.removeEventListener("change",g))}}const d=T[h-1];if(d&&s[d]!==void 0){const p=s[d];if(Number.isNaN(p))throw new Error(`Invalid breakpoint value for ${d}: ${s[d]}`);{const m=window.matchMedia(`(max-width: ${p-1}px)`);i.push(m);const g=()=>u();m.addEventListener("change",g),a.push(()=>m.removeEventListener("change",g))}}};return u(),()=>{a.forEach(s=>s())}},[t]),r}export{tt as ArrayRender,Re as Boundary,ct as Counter,nt as DateRender,me as DefBreakpointDesc,Fe as False,ce as FocusTrap,N as If,Ie as Observer,Ae as Pipe,$e as Portal,Me as Repeat,xe as Scope,De as SizeBox,V as Styles,x as Switch,ke as Toggle,Ne as True,Ce as When,T as breakpoints,Q as childrenLoop,de as createExternalState,lt as createStorageState,B as cx,ut as formatDate,K as getCurrentBreakpoint,Ke as getFocusableElements,q as getTabIndex,ue as getTabbableElements,Ge as isFocusable,Xe as isTabbable,mt as ruleChecker,fe as safePromiseTry,st as safePromiseWithResolvers,it as useControlled,pt as useScreen};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwog/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "A practical React component library providing declarative flow control and common UI utility components to make your React code more concise and readable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { expect, describe, it, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { render } from 'vitest-browser-react'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { FocusTrap, type FocusDirection } from './FocusTrap'
|
|
5
|
+
|
|
6
|
+
function toEl(e: { element(): Element }): HTMLElement {
|
|
7
|
+
return e.element() as HTMLElement
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
describe('FocusTrap', () => {
|
|
13
|
+
let container: HTMLDivElement
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
container = document.createElement('div')
|
|
17
|
+
document.body.appendChild(container)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
container.remove()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('默认 Tab/Shift+Tab 在可聚焦元素间循环', async () => {
|
|
25
|
+
const { getByTestId } = render(
|
|
26
|
+
<FocusTrap>
|
|
27
|
+
<input data-testid="input1" />
|
|
28
|
+
<button data-testid="btn1">Button 1</button>
|
|
29
|
+
<button data-testid="btn2">Button 2</button>
|
|
30
|
+
</FocusTrap>,
|
|
31
|
+
{ container },
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const input1 = toEl(getByTestId('input1'))
|
|
35
|
+
const btn1 = toEl(getByTestId('btn1'))
|
|
36
|
+
const btn2 = toEl(getByTestId('btn2'))
|
|
37
|
+
|
|
38
|
+
input1.focus()
|
|
39
|
+
expect(document.activeElement).toBe(input1)
|
|
40
|
+
|
|
41
|
+
btn1.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
42
|
+
expect(document.activeElement).toBe(btn1)
|
|
43
|
+
|
|
44
|
+
btn1.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
45
|
+
expect(document.activeElement).toBe(btn2)
|
|
46
|
+
|
|
47
|
+
btn2.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
48
|
+
expect(document.activeElement).toBe(input1)
|
|
49
|
+
|
|
50
|
+
input1.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true }))
|
|
51
|
+
expect(document.activeElement).toBe(btn2)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('disabled 时取消焦点劫持', async () => {
|
|
55
|
+
const { getByTestId } = render(
|
|
56
|
+
<FocusTrap disabled>
|
|
57
|
+
<input data-testid="input1" />
|
|
58
|
+
<button data-testid="btn1">Button</button>
|
|
59
|
+
</FocusTrap>,
|
|
60
|
+
{ container },
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const btn1 = toEl(getByTestId('btn1'))
|
|
64
|
+
|
|
65
|
+
const handled = btn1.dispatchEvent(
|
|
66
|
+
new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true }),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
expect(handled).toBe(true)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('keyMap 支持自定义按键导航', async () => {
|
|
73
|
+
const { getByTestId } = render(
|
|
74
|
+
<FocusTrap keyMap={{ ArrowDown: 'next', ArrowUp: 'prev' }}>
|
|
75
|
+
<input data-testid="input1" />
|
|
76
|
+
<button data-testid="btn1">Button 1</button>
|
|
77
|
+
<button data-testid="btn2">Button 2</button>
|
|
78
|
+
</FocusTrap>,
|
|
79
|
+
{ container },
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const input1 = toEl(getByTestId('input1'))
|
|
83
|
+
const btn1 = toEl(getByTestId('btn1'))
|
|
84
|
+
const btn2 = toEl(getByTestId('btn2'))
|
|
85
|
+
|
|
86
|
+
input1.focus()
|
|
87
|
+
|
|
88
|
+
input1.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
|
|
89
|
+
expect(document.activeElement).toBe(btn1)
|
|
90
|
+
|
|
91
|
+
btn1.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
|
|
92
|
+
expect(document.activeElement).toBe(btn2)
|
|
93
|
+
|
|
94
|
+
btn2.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }))
|
|
95
|
+
expect(document.activeElement).toBe(btn1)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('keyMap 支持左右方向键导航', async () => {
|
|
99
|
+
const { getByTestId } = render(
|
|
100
|
+
<FocusTrap keyMap={{ ArrowRight: 'next', ArrowLeft: 'prev' }}>
|
|
101
|
+
<input data-testid="input1" />
|
|
102
|
+
<button data-testid="btn1">Button 1</button>
|
|
103
|
+
</FocusTrap>,
|
|
104
|
+
{ container },
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const input1 = toEl(getByTestId('input1'))
|
|
108
|
+
const btn1 = toEl(getByTestId('btn1'))
|
|
109
|
+
|
|
110
|
+
input1.focus()
|
|
111
|
+
|
|
112
|
+
input1.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
|
|
113
|
+
expect(document.activeElement).toBe(btn1)
|
|
114
|
+
|
|
115
|
+
btn1.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }))
|
|
116
|
+
expect(document.activeElement).toBe(input1)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('keyMap 可与 Tab 共存', async () => {
|
|
120
|
+
const { getByTestId } = render(
|
|
121
|
+
<FocusTrap keyMap={{ ArrowDown: 'next' }}>
|
|
122
|
+
<input data-testid="input1" />
|
|
123
|
+
<button data-testid="btn1">Button 1</button>
|
|
124
|
+
</FocusTrap>,
|
|
125
|
+
{ container },
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const input1 = toEl(getByTestId('input1'))
|
|
129
|
+
const btn1 = toEl(getByTestId('btn1'))
|
|
130
|
+
|
|
131
|
+
input1.focus()
|
|
132
|
+
|
|
133
|
+
input1.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
134
|
+
expect(document.activeElement).toBe(btn1)
|
|
135
|
+
|
|
136
|
+
btn1.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
|
|
137
|
+
expect(document.activeElement).toBe(input1)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('onNavigate 支持自定义焦点解析', async () => {
|
|
141
|
+
const navigate = (
|
|
142
|
+
_current: HTMLElement | null,
|
|
143
|
+
_elements: HTMLElement[],
|
|
144
|
+
direction: FocusDirection,
|
|
145
|
+
) => {
|
|
146
|
+
if (direction === 'next') return _elements[_elements.length - 1]
|
|
147
|
+
return _elements[0]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { getByTestId } = render(
|
|
151
|
+
<FocusTrap onNavigate={navigate}>
|
|
152
|
+
<input data-testid="input1" />
|
|
153
|
+
<button data-testid="btn1">Button 1</button>
|
|
154
|
+
<button data-testid="btn2">Button 2</button>
|
|
155
|
+
</FocusTrap>,
|
|
156
|
+
{ container },
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const input1 = toEl(getByTestId('input1'))
|
|
160
|
+
const btn2 = toEl(getByTestId('btn2'))
|
|
161
|
+
|
|
162
|
+
input1.focus()
|
|
163
|
+
|
|
164
|
+
input1.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
165
|
+
expect(document.activeElement).toBe(btn2)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('autoFocus 挂载时自动聚焦第一个元素', async () => {
|
|
169
|
+
const { getByTestId } = render(
|
|
170
|
+
<FocusTrap autoFocus>
|
|
171
|
+
<input data-testid="input1" />
|
|
172
|
+
<button data-testid="btn1">Button</button>
|
|
173
|
+
</FocusTrap>,
|
|
174
|
+
{ container },
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const input1 = toEl(getByTestId('input1'))
|
|
178
|
+
await vi.waitFor(() => {
|
|
179
|
+
expect(document.activeElement).toBe(input1)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('空 tabbable 元素时不报错', async () => {
|
|
184
|
+
const { getByTestId } = render(
|
|
185
|
+
<FocusTrap>
|
|
186
|
+
<div data-testid="div1">No focusable</div>
|
|
187
|
+
</FocusTrap>,
|
|
188
|
+
{ container },
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const div1 = toEl(getByTestId('div1'))
|
|
192
|
+
div1.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('focusableOptions 透传给 getTabbableElements', async () => {
|
|
196
|
+
const captured: { elements: HTMLElement[] } = { elements: [] }
|
|
197
|
+
|
|
198
|
+
const { getByTestId } = render(
|
|
199
|
+
<FocusTrap
|
|
200
|
+
focusableOptions={{ displayCheck: 'none' }}
|
|
201
|
+
onNavigate={(current, elements) => {
|
|
202
|
+
captured.elements = elements
|
|
203
|
+
return current
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<button data-testid="btn1" style={{ display: 'none' }}>
|
|
207
|
+
Hidden
|
|
208
|
+
</button>
|
|
209
|
+
<button data-testid="btn2">Visible</button>
|
|
210
|
+
</FocusTrap>,
|
|
211
|
+
{ container },
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const btn2 = toEl(getByTestId('btn2'))
|
|
215
|
+
btn2.focus()
|
|
216
|
+
btn2.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
217
|
+
|
|
218
|
+
expect(captured.elements.length).toBe(2)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('未被 keyMap 覆盖的按键不拦截', async () => {
|
|
222
|
+
const { getByTestId } = render(
|
|
223
|
+
<FocusTrap keyMap={{ ArrowDown: 'next' }}>
|
|
224
|
+
<input data-testid="input1" />
|
|
225
|
+
<button data-testid="btn1">Button</button>
|
|
226
|
+
</FocusTrap>,
|
|
227
|
+
{ container },
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
const btn1 = toEl(getByTestId('btn1'))
|
|
231
|
+
|
|
232
|
+
const handled = btn1.dispatchEvent(
|
|
233
|
+
new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true }),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
expect(handled).toBe(true)
|
|
237
|
+
})
|
|
238
|
+
})
|