@astroapps/aria-base 1.4.0 → 1.5.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.
@@ -0,0 +1,130 @@
1
+ import React from "react";
2
+ import {
3
+ ListBox as AriaListBox,
4
+ ListBoxItem as AriaListBoxItem,
5
+ ListBoxProps as AriaListBoxProps,
6
+ Collection,
7
+ Header,
8
+ ListBoxItemProps,
9
+ ListBoxSection,
10
+ SectionProps,
11
+ composeRenderProps,
12
+ } from "react-aria-components";
13
+ import { tv } from "tailwind-variants";
14
+ import { composeTailwindRenderProps, focusRing } from "./utils";
15
+
16
+ interface ListBoxProps<T>
17
+ extends Omit<AriaListBoxProps<T>, "layout" | "orientation"> {}
18
+
19
+ export function ListBox<T extends object>({
20
+ children,
21
+ ...props
22
+ }: ListBoxProps<T>) {
23
+ return (
24
+ <AriaListBox
25
+ {...props}
26
+ className={composeTailwindRenderProps(
27
+ props.className,
28
+ "outline-0 p-1 w-[200px] bg-white dark:bg-neutral-900 border border-neutral-300 dark:border-neutral-700 rounded-lg font-sans",
29
+ )}
30
+ >
31
+ {children}
32
+ </AriaListBox>
33
+ );
34
+ }
35
+
36
+ export const itemStyles = tv({
37
+ extend: focusRing,
38
+ base: "group relative flex items-center gap-8 cursor-default select-none py-1.5 px-2.5 rounded-md will-change-transform text-sm forced-color-adjust-none",
39
+ variants: {
40
+ isSelected: {
41
+ false:
42
+ "text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 pressed:bg-neutral-100 dark:hover:bg-neutral-800 dark:pressed:bg-neutral-800 -outline-offset-2",
43
+ true: "bg-secondary-600 text-white forced-colors:bg-[Highlight] forced-colors:text-[HighlightText] [&:has(+[data-selected])]:rounded-b-none [&+[data-selected]]:rounded-t-none -outline-offset-4 outline-white dark:outline-white forced-colors:outline-[HighlightText]",
44
+ },
45
+ isDisabled: {
46
+ true: "text-neutral-300 dark:text-neutral-600 forced-colors:text-[GrayText]",
47
+ },
48
+ },
49
+ });
50
+
51
+ export function ListBoxItem(props: ListBoxItemProps) {
52
+ let textValue =
53
+ props.textValue ||
54
+ (typeof props.children === "string" ? props.children : undefined);
55
+ return (
56
+ <AriaListBoxItem {...props} textValue={textValue} className={itemStyles}>
57
+ {composeRenderProps(props.children, (children) => (
58
+ <>
59
+ {children}
60
+ <div className="absolute left-4 right-4 bottom-0 h-px bg-white/20 forced-colors:bg-[HighlightText] hidden [.group[data-selected]:has(+[data-selected])_&]:block" />
61
+ </>
62
+ ))}
63
+ </AriaListBoxItem>
64
+ );
65
+ }
66
+
67
+ export const dropdownItemStyles = tv({
68
+ base: "group flex items-center gap-4 cursor-default select-none py-2 pl-3 pr-3 selected:pr-1 rounded-lg outline outline-0 text-sm forced-color-adjust-none no-underline [&[href]]:cursor-pointer [-webkit-tap-highlight-color:transparent]",
69
+ variants: {
70
+ isDisabled: {
71
+ false: "text-neutral-900 dark:text-neutral-100",
72
+ true: "text-neutral-300 dark:text-neutral-600 forced-colors:text-[GrayText]",
73
+ },
74
+ isPressed: {
75
+ true: "bg-neutral-100 dark:bg-neutral-800",
76
+ },
77
+ isFocused: {
78
+ true: "bg-secondary-600 dark:bg-secondary-600 text-white forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
79
+ },
80
+ },
81
+ compoundVariants: [
82
+ {
83
+ isFocused: false,
84
+ isOpen: true,
85
+ className: "bg-neutral-100 dark:bg-neutral-700/60",
86
+ },
87
+ ],
88
+ });
89
+
90
+ export function DropdownItem(props: ListBoxItemProps) {
91
+ let textValue =
92
+ props.textValue ||
93
+ (typeof props.children === "string" ? props.children : undefined);
94
+ return (
95
+ <AriaListBoxItem
96
+ {...props}
97
+ textValue={textValue}
98
+ className={dropdownItemStyles}
99
+ >
100
+ {composeRenderProps(props.children, (children, { isSelected }) => (
101
+ <>
102
+ <span className="flex items-center flex-1 gap-2 font-normal truncate group-selected:font-semibold">
103
+ {children}
104
+ </span>
105
+ <span className="flex items-center w-5">
106
+ {isSelected && <i className="w-4 h-4 fa fa-check" />}
107
+ </span>
108
+ </>
109
+ ))}
110
+ </AriaListBoxItem>
111
+ );
112
+ }
113
+
114
+ export interface DropdownSectionProps<T> extends SectionProps<T> {
115
+ title?: string;
116
+ items?: any;
117
+ }
118
+
119
+ export function DropdownSection<T extends object>(
120
+ props: DropdownSectionProps<T>,
121
+ ) {
122
+ return (
123
+ <ListBoxSection className="first:-mt-[5px] after:content-[''] after:block after:h-[5px] last:after:hidden">
124
+ <Header className="text-sm font-semibold text-neutral-500 dark:text-neutral-300 px-4 py-1 truncate sticky -top-[5px] -mt-px -mx-1 z-10 bg-neutral-100/60 dark:bg-neutral-700/60 backdrop-blur-md supports-[-moz-appearance:none]:bg-neutral-100 border-y border-y-neutral-200 dark:border-y-neutral-700 [&+*]:mt-1">
125
+ {props.title}
126
+ </Header>
127
+ <Collection items={props.items}>{props.children}</Collection>
128
+ </ListBoxSection>
129
+ );
130
+ }
package/src/Popover.tsx CHANGED
@@ -1,65 +1,72 @@
1
- import type { AriaPopoverProps } from "react-aria";
2
- import { DismissButton, Overlay, usePopover } from "react-aria";
3
- import type { OverlayTriggerState } from "react-stately";
4
- import React, { ReactNode, useRef } from "react";
5
- import { DOMAttributes } from "@react-types/shared";
1
+ import {
2
+ OverlayArrow,
3
+ Popover as AriaPopover,
4
+ PopoverProps as AriaPopoverProps,
5
+ composeRenderProps,
6
+ } from "react-aria-components";
7
+ import React from "react";
8
+ import { tv } from "tailwind-variants";
9
+ import { OverlayTriggerState } from "react-stately";
6
10
 
7
- export interface PopoverClasses {
8
- underlayClass?: string;
9
- popoverClass?: string;
10
- }
11
- export interface PopoverProps
12
- extends Omit<AriaPopoverProps, "popoverRef">,
13
- PopoverClasses {
11
+ export interface PopoverProps extends Omit<AriaPopoverProps, "children"> {
12
+ showArrow?: boolean;
14
13
  children: React.ReactNode;
15
- state: OverlayTriggerState;
16
- portalContainer?: Element;
17
- renderArrow?: (props: DOMAttributes) => ReactNode;
18
14
  }
19
15
 
20
- export const DefaultPopoverClasses = {
21
- underlayClass: "fixed inset-0",
22
- popoverClass: "bg-white",
23
- };
16
+ const styles = tv({
17
+ base: "font-sans bg-white dark:bg-neutral-900/70 dark:backdrop-blur-2xl dark:backdrop-saturate-200 forced-colors:bg-[Canvas] shadow-2xl rounded-xl bg-clip-padding border border-black/10 dark:border-white/10 text-neutral-700 dark:text-neutral-300 outline-0",
18
+ variants: {
19
+ isEntering: {
20
+ true: "animate-in fade-in placement-bottom:slide-in-from-top-1 placement-top:slide-in-from-bottom-1 placement-left:slide-in-from-right-1 placement-right:slide-in-from-left-1 ease-out duration-200",
21
+ },
22
+ isExiting: {
23
+ true: "animate-out fade-out placement-bottom:slide-out-to-top-1 placement-top:slide-out-to-bottom-1 placement-left:slide-out-to-right-1 placement-right:slide-out-to-left-1 ease-in duration-150",
24
+ },
25
+ },
26
+ });
24
27
 
25
28
  export function Popover({
26
29
  children,
27
- state,
28
- renderArrow,
29
- portalContainer,
30
+ showArrow,
31
+ className,
30
32
  ...props
31
33
  }: PopoverProps) {
32
- let popoverRef = useRef(null);
33
- const { popoverClass, underlayClass } = {
34
- ...DefaultPopoverClasses,
35
- ...props,
36
- };
37
- let { popoverProps, underlayProps, arrowProps } = usePopover(
38
- {
39
- ...props,
40
- popoverRef,
41
- },
42
- state,
43
- );
44
-
34
+ let offset = showArrow ? 12 : 8;
45
35
  return (
46
- <Overlay portalContainer={portalContainer}>
47
- <div {...underlayProps} className={underlayClass} />
48
- <div
49
- {...popoverProps}
50
- ref={popoverRef}
51
- className={popoverClass}
52
- style={popoverProps.style}
53
- >
54
- {renderArrow?.(arrowProps)}
55
- <DismissButton onDismiss={state.close} />
56
- {children}
57
- <DismissButton onDismiss={state.close} />
58
- </div>
59
- </Overlay>
36
+ <AriaPopover
37
+ offset={offset}
38
+ {...props}
39
+ className={composeRenderProps(className, (className, renderProps) =>
40
+ styles({ ...renderProps, className }),
41
+ )}
42
+ >
43
+ {showArrow && (
44
+ <OverlayArrow className="group">
45
+ <svg
46
+ width={12}
47
+ height={12}
48
+ viewBox="0 0 12 12"
49
+ className="block fill-white dark:fill-[#1f1f21] forced-colors:fill-[Canvas] stroke-1 stroke-black/10 dark:stroke-neutral-700 forced-colors:stroke-[ButtonBorder] group-placement-bottom:rotate-180 group-placement-left:-rotate-90 group-placement-right:rotate-90"
50
+ >
51
+ <path d="M0 0 L6 6 L12 0" />
52
+ </svg>
53
+ </OverlayArrow>
54
+ )}
55
+ {children}
56
+ </AriaPopover>
60
57
  );
61
58
  }
62
59
 
60
+ export interface PopoverClasses {
61
+ underlayClass?: string;
62
+ popoverClass?: string;
63
+ }
64
+
65
+ export const DefaultPopoverClasses = {
66
+ underlayClass: "fixed inset-0",
67
+ popoverClass: "bg-white",
68
+ };
69
+
63
70
  export function createOverlayState(ref: {
64
71
  value: boolean;
65
72
  }): OverlayTriggerState {
package/src/Select.tsx ADDED
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+ import {
3
+ Select as AriaSelect,
4
+ SelectProps as AriaSelectProps,
5
+ Button,
6
+ ListBox,
7
+ ListBoxItemProps,
8
+ SelectValue,
9
+ ValidationResult,
10
+ } from "react-aria-components";
11
+ import { tv } from "tailwind-variants";
12
+ import { Description, FieldError, Label } from "./Field";
13
+ import { DropdownItem, DropdownSection, DropdownSectionProps } from "./ListBox";
14
+ import { Popover } from "./Popover";
15
+ import { composeTailwindRenderProps, focusRing } from "./utils";
16
+
17
+ const styles = tv({
18
+ extend: focusRing,
19
+ base: "flex items-center text-start gap-4 w-full font-sans border border-black/10 dark:border-white/10 cursor-default rounded-lg pl-3 pr-2 h-9 min-w-[64px] transition bg-neutral-50 dark:bg-neutral-700 [-webkit-tap-highlight-color:transparent]",
20
+ variants: {
21
+ isDisabled: {
22
+ false:
23
+ "text-neutral-800 dark:text-neutral-300 hover:bg-neutral-100 pressed:bg-neutral-200 dark:hover:bg-neutral-600 dark:pressed:bg-neutral-500 group-invalid:outline group-invalid:outline-red-600 forced-colors:group-invalid:outline-[Mark]",
24
+ true: "border-transparent dark:border-transparent text-neutral-200 dark:text-neutral-600 forced-colors:text-[GrayText] bg-neutral-100 dark:bg-neutral-800",
25
+ },
26
+ },
27
+ });
28
+
29
+ export interface SelectProps<T extends object>
30
+ extends Omit<AriaSelectProps<T>, "children"> {
31
+ label?: string;
32
+ description?: string;
33
+ errorMessage?: string | ((validation: ValidationResult) => string);
34
+ items?: Iterable<T>;
35
+ children: React.ReactNode | ((item: T) => React.ReactNode);
36
+ }
37
+
38
+ export function Select<T extends object>({
39
+ label,
40
+ description,
41
+ errorMessage,
42
+ children,
43
+ items,
44
+ ...props
45
+ }: SelectProps<T>) {
46
+ return (
47
+ <AriaSelect
48
+ {...props}
49
+ className={composeTailwindRenderProps(
50
+ props.className,
51
+ "group flex flex-col gap-1 relative font-sans",
52
+ )}
53
+ >
54
+ {label && <Label>{label}</Label>}
55
+ <Button className={styles}>
56
+ <SelectValue className="flex-1 text-sm">
57
+ {({ selectedText, defaultChildren }) =>
58
+ selectedText || defaultChildren
59
+ }
60
+ </SelectValue>
61
+ <i
62
+ aria-hidden
63
+ className={
64
+ "w-4 h-4 fa fa-chevron-down text-neutral-600 dark:text-neutral-400 forced-colors:text-[ButtonText] group-disabled:text-neutral-200 dark:group-disabled:text-neutral-600 forced-colors:group-disabled:text-[GrayText]"
65
+ }
66
+ />
67
+ </Button>
68
+ {description && <Description>{description}</Description>}
69
+ <FieldError>{errorMessage}</FieldError>
70
+ <Popover className="min-w-(--trigger-width)">
71
+ <ListBox
72
+ items={items}
73
+ className="outline-hidden box-border p-1 max-h-[inherit] overflow-auto [clip-path:inset(0_0_0_0_round_.75rem)]"
74
+ >
75
+ {children}
76
+ </ListBox>
77
+ </Popover>
78
+ </AriaSelect>
79
+ );
80
+ }
81
+
82
+ export function SelectItem(props: ListBoxItemProps) {
83
+ return <DropdownItem {...props} />;
84
+ }
85
+
86
+ export function SelectSection<T extends object>(
87
+ props: DropdownSectionProps<T>,
88
+ ) {
89
+ return <DropdownSection {...props} />;
90
+ }
package/src/index.ts CHANGED
@@ -2,3 +2,6 @@ export * from "./Button";
2
2
  export * from "./Dialog";
3
3
  export * from "./Popover";
4
4
  export * from "./Modal";
5
+ export * from "./Select";
6
+ export * from "./utils";
7
+ export * from "./Field";
package/src/utils.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { composeRenderProps } from "react-aria-components";
2
+ import { twMerge } from "tailwind-merge";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ export const focusRing = tv({
6
+ base: "outline outline-secondary-600 dark:outline-secondary-500 forced-colors:outline-[Highlight] outline-offset-2",
7
+ variants: {
8
+ isFocusVisible: {
9
+ false: "outline-0",
10
+ true: "outline-2",
11
+ },
12
+ },
13
+ });
14
+
15
+ export function composeTailwindRenderProps<T>(
16
+ className: string | ((v: T) => string) | undefined,
17
+ tw: string,
18
+ ): string | ((v: T) => string) {
19
+ return composeRenderProps(className, (className) => twMerge(tw, className));
20
+ }