@fpkit/acss 6.1.0 → 6.3.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/libs/chunk-25KCUE3R.cjs +17 -0
- package/libs/chunk-25KCUE3R.cjs.map +1 -0
- package/libs/chunk-34NWHFHP.js +10 -0
- package/libs/chunk-34NWHFHP.js.map +1 -0
- package/libs/{chunk-SQ44OCJ2.js → chunk-6NMLU5FA.js} +2 -2
- package/libs/{chunk-GVVCXXKI.cjs → chunk-6YVR4TDM.cjs} +3 -3
- package/libs/chunk-DSQ2TUCR.js +7 -0
- package/libs/chunk-DSQ2TUCR.js.map +1 -0
- package/libs/{chunk-H6A2CUWA.js → chunk-VQTCTLFN.js} +2 -2
- package/libs/chunk-ZJ4RUKI2.cjs +14 -0
- package/libs/chunk-ZJ4RUKI2.cjs.map +1 -0
- package/libs/{chunk-H4JRUNKU.cjs → chunk-ZOPHCNFD.cjs} +3 -3
- package/libs/components/button.cjs +3 -3
- package/libs/components/button.d.cts +34 -1
- package/libs/components/button.d.ts +34 -1
- package/libs/components/button.js +1 -1
- package/libs/components/buttons/button.css +1 -1
- package/libs/components/buttons/button.css.map +1 -1
- package/libs/components/buttons/button.min.css +2 -2
- package/libs/components/buttons/icon-button.css +1 -0
- package/libs/components/buttons/icon-button.css.map +1 -0
- package/libs/components/buttons/icon-button.min.css +3 -0
- package/libs/components/dialog/dialog.cjs +4 -4
- package/libs/components/dialog/dialog.js +2 -2
- package/libs/components/icons/icon.d.cts +1 -1
- package/libs/components/icons/icon.d.ts +1 -1
- package/libs/components/layout/landmarks.css +1 -1
- package/libs/components/layout/landmarks.css.map +1 -1
- package/libs/components/layout/landmarks.min.css +2 -2
- package/libs/components/link/link.css +1 -1
- package/libs/components/link/link.min.css +1 -1
- package/libs/components/modal.cjs +3 -3
- package/libs/components/modal.js +2 -2
- package/libs/components/popover/popover.cjs +3 -8
- package/libs/components/popover/popover.css +1 -0
- package/libs/components/popover/popover.css.map +1 -0
- package/libs/components/popover/popover.d.cts +54 -26
- package/libs/components/popover/popover.d.ts +54 -26
- package/libs/components/popover/popover.js +1 -2
- package/libs/components/popover/popover.min.css +3 -0
- package/libs/hooks.cjs +3 -6
- package/libs/hooks.cjs.map +1 -1
- package/libs/hooks.d.cts +30 -10
- package/libs/hooks.d.ts +30 -10
- package/libs/hooks.js +5 -1
- package/libs/hooks.js.map +1 -1
- package/libs/{icons-48788561.d.ts → icons-2c09535c.d.ts} +32 -32
- package/libs/icons.d.cts +1 -1
- package/libs/icons.d.ts +1 -1
- package/libs/index.cjs +41 -40
- package/libs/index.cjs.map +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +101 -5
- package/libs/index.d.ts +101 -5
- package/libs/index.js +14 -15
- package/libs/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/buttons/README.mdx +107 -11
- package/src/components/buttons/STYLES.mdx +182 -47
- package/src/components/buttons/button.scss +93 -16
- package/src/components/buttons/button.stories.tsx +149 -0
- package/src/components/buttons/button.test.tsx +12 -0
- package/src/components/buttons/button.tsx +50 -6
- package/src/components/buttons/icon-button.scss +45 -0
- package/src/components/buttons/icon-button.stories.tsx +200 -0
- package/src/components/buttons/icon-button.test.tsx +132 -0
- package/src/components/buttons/icon-button.tsx +72 -0
- package/src/components/form/select.tsx +55 -51
- package/src/components/layout/README.mdx +1117 -0
- package/src/components/layout/STYLES.mdx +159 -4
- package/src/components/layout/fieldset.stories.tsx +387 -0
- package/src/components/layout/landmarks.scss +115 -2
- package/src/components/layout/landmarks.stories.tsx +2 -6
- package/src/components/layout/landmarks.tsx +96 -27
- package/src/components/link/link.scss +2 -2
- package/src/components/popover/README.mdx +478 -0
- package/src/components/popover/STYLES.mdx +389 -0
- package/src/components/popover/index.ts +3 -0
- package/src/components/popover/popover.scss +249 -0
- package/src/components/popover/popover.stories.tsx +315 -15
- package/src/components/popover/popover.test.tsx +249 -37
- package/src/components/popover/popover.tsx +165 -62
- package/src/hooks/popover/popover.tsx +26 -10
- package/src/hooks/popover/use-popover.tsx +30 -10
- package/src/hooks.ts +5 -0
- package/src/index.scss +1 -0
- package/src/index.ts +1 -0
- package/src/styles/buttons/button.css +78 -16
- package/src/styles/buttons/button.css.map +1 -1
- package/src/styles/buttons/icon-button.css +32 -0
- package/src/styles/buttons/icon-button.css.map +1 -0
- package/src/styles/index.css +350 -18
- package/src/styles/index.css.map +1 -1
- package/src/styles/layout/landmarks.css +83 -0
- package/src/styles/layout/landmarks.css.map +1 -1
- package/src/styles/link/link.css +2 -2
- package/src/styles/popover/popover.css +190 -0
- package/src/styles/popover/popover.css.map +1 -0
- package/src/types/popover.d.ts +64 -0
- package/libs/chunk-4I5MF54P.js +0 -8
- package/libs/chunk-4I5MF54P.js.map +0 -1
- package/libs/chunk-GCGKYLDG.js +0 -7
- package/libs/chunk-GCGKYLDG.js.map +0 -1
- package/libs/chunk-NZVSXRTB.cjs +0 -16
- package/libs/chunk-NZVSXRTB.cjs.map +0 -1
- package/libs/chunk-PDD4N5P5.cjs +0 -10
- package/libs/chunk-PDD4N5P5.cjs.map +0 -1
- package/libs/chunk-S7NIA6PI.cjs +0 -17
- package/libs/chunk-S7NIA6PI.cjs.map +0 -1
- package/libs/chunk-X2RDXWH5.js +0 -10
- package/libs/chunk-X2RDXWH5.js.map +0 -1
- /package/libs/{chunk-SQ44OCJ2.js.map → chunk-6NMLU5FA.js.map} +0 -0
- /package/libs/{chunk-GVVCXXKI.cjs.map → chunk-6YVR4TDM.cjs.map} +0 -0
- /package/libs/{chunk-H6A2CUWA.js.map → chunk-VQTCTLFN.js.map} +0 -0
- /package/libs/{chunk-H4JRUNKU.cjs.map → chunk-ZOPHCNFD.cjs.map} +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { vi } from "vitest";
|
|
5
|
+
import { IconButton } from "./icon-button";
|
|
6
|
+
|
|
7
|
+
const TestIcon = () => <svg data-testid="test-icon" aria-hidden="true" />;
|
|
8
|
+
|
|
9
|
+
describe("IconButton", () => {
|
|
10
|
+
it("renders a button element with aria-label", () => {
|
|
11
|
+
render(<IconButton type="button" aria-label="Close" icon={<TestIcon />} />);
|
|
12
|
+
const button = screen.getByRole("button", { name: "Close" });
|
|
13
|
+
expect(button).toBeInTheDocument();
|
|
14
|
+
expect(button).toHaveAttribute("aria-label", "Close");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("renders a button element with aria-labelledby", () => {
|
|
18
|
+
render(
|
|
19
|
+
<>
|
|
20
|
+
<span id="lbl">Delete item</span>
|
|
21
|
+
<IconButton type="button" aria-labelledby="lbl" icon={<TestIcon />} />
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
const button = screen.getByRole("button", { name: "Delete item" });
|
|
25
|
+
expect(button).toBeInTheDocument();
|
|
26
|
+
expect(button).toHaveAttribute("aria-labelledby", "lbl");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders the icon as a child of the button", () => {
|
|
30
|
+
render(<IconButton type="button" aria-label="Close" icon={<TestIcon />} />);
|
|
31
|
+
expect(screen.getByTestId("test-icon")).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders label text when label prop is provided", () => {
|
|
35
|
+
render(
|
|
36
|
+
<IconButton
|
|
37
|
+
type="button"
|
|
38
|
+
aria-label="Settings"
|
|
39
|
+
icon={<TestIcon />}
|
|
40
|
+
label="Settings"
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
expect(screen.getByText("Settings")).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("applies data-icon-label attribute to the label span", () => {
|
|
47
|
+
render(
|
|
48
|
+
<IconButton
|
|
49
|
+
type="button"
|
|
50
|
+
aria-label="Settings"
|
|
51
|
+
icon={<TestIcon />}
|
|
52
|
+
label="Settings"
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
const labelSpan = screen.getByText("Settings");
|
|
56
|
+
expect(labelSpan).toHaveAttribute("data-icon-label");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("applies data-icon-btn='has-label' to the button when label is provided", () => {
|
|
60
|
+
render(
|
|
61
|
+
<IconButton
|
|
62
|
+
type="button"
|
|
63
|
+
aria-label="Settings"
|
|
64
|
+
icon={<TestIcon />}
|
|
65
|
+
label="Settings"
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
const button = screen.getByRole("button", { name: "Settings" });
|
|
69
|
+
expect(button).toHaveAttribute("data-icon-btn", "has-label");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not render a label span when label prop is omitted", () => {
|
|
73
|
+
render(<IconButton type="button" aria-label="Close" icon={<TestIcon />} />);
|
|
74
|
+
expect(document.querySelector("[data-icon-label]")).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("sets data-icon-btn to 'icon' when label is omitted", () => {
|
|
78
|
+
render(<IconButton type="button" aria-label="Close" icon={<TestIcon />} />);
|
|
79
|
+
const button = screen.getByRole("button", { name: "Close" });
|
|
80
|
+
expect(button).toHaveAttribute("data-icon-btn", "icon");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("fires the click handler when clicked", async () => {
|
|
84
|
+
const handleClick = vi.fn();
|
|
85
|
+
render(
|
|
86
|
+
<IconButton
|
|
87
|
+
type="button"
|
|
88
|
+
aria-label="Close"
|
|
89
|
+
icon={<TestIcon />}
|
|
90
|
+
onClick={handleClick}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
await userEvent.click(screen.getByRole("button", { name: "Close" }));
|
|
94
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not fire click handler when disabled", async () => {
|
|
98
|
+
const handleClick = vi.fn();
|
|
99
|
+
render(
|
|
100
|
+
<IconButton
|
|
101
|
+
type="button"
|
|
102
|
+
aria-label="Close"
|
|
103
|
+
icon={<TestIcon />}
|
|
104
|
+
disabled
|
|
105
|
+
onClick={handleClick}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
const button = screen.getByRole("button", { name: "Close" });
|
|
109
|
+
expect(button).toHaveAttribute("aria-disabled", "true");
|
|
110
|
+
await userEvent.click(button);
|
|
111
|
+
expect(handleClick).toHaveBeenCalledTimes(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("defaults variant to 'icon'", () => {
|
|
115
|
+
render(<IconButton type="button" aria-label="Close" icon={<TestIcon />} />);
|
|
116
|
+
const button = screen.getByRole("button", { name: "Close" });
|
|
117
|
+
expect(button).toHaveAttribute("data-style", "icon");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("accepts a variant override", () => {
|
|
121
|
+
render(
|
|
122
|
+
<IconButton
|
|
123
|
+
type="button"
|
|
124
|
+
aria-label="Settings"
|
|
125
|
+
icon={<TestIcon />}
|
|
126
|
+
variant="outline"
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
const button = screen.getByRole("button", { name: "Settings" });
|
|
130
|
+
expect(button).toHaveAttribute("data-style", "outline");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button, type ButtonProps } from "./button";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* XOR constraint: exactly one of aria-label or aria-labelledby is required.
|
|
6
|
+
* Passing both or neither is a TypeScript compile-time error.
|
|
7
|
+
* Satisfies WCAG 2.1 SC 1.1.1 (Non-text Content).
|
|
8
|
+
*/
|
|
9
|
+
type WithAriaLabel = { "aria-label": string; "aria-labelledby"?: never };
|
|
10
|
+
type WithAriaLabelledBy = { "aria-labelledby": string; "aria-label"?: never };
|
|
11
|
+
|
|
12
|
+
export type IconButtonProps = Omit<ButtonProps, "children"> &
|
|
13
|
+
(WithAriaLabel | WithAriaLabelledBy) & {
|
|
14
|
+
/** The icon element rendered inside the button. */
|
|
15
|
+
icon: React.ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Optional text shown alongside the icon at desktop widths.
|
|
18
|
+
* Hidden visually below the `$icon-label-bp` SCSS breakpoint (default 48rem / 768px),
|
|
19
|
+
* but remains in the accessibility tree — screen readers always announce it.
|
|
20
|
+
*
|
|
21
|
+
* NOTE: When `label` is used, the default `variant="icon"` removes padding.
|
|
22
|
+
* Override with a different variant (e.g. `variant="outline"`) for a padded layout.
|
|
23
|
+
*/
|
|
24
|
+
label?: string;
|
|
25
|
+
/** Button type: button, submit, or reset. Required. */
|
|
26
|
+
type: "button" | "submit" | "reset";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Accessible icon button component. Wraps `Button` with:
|
|
31
|
+
* - Required accessible label via `aria-label` or `aria-labelledby` (XOR enforced)
|
|
32
|
+
* - Optional visible `label` text that hides on mobile (visual only — always in a11y tree)
|
|
33
|
+
* - `variant="icon"` default (square, no padding)
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Icon only
|
|
37
|
+
* <IconButton type="button" aria-label="Close menu" icon={<CloseIcon />} />
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Icon + label (label hides on mobile)
|
|
41
|
+
* <IconButton
|
|
42
|
+
* type="button"
|
|
43
|
+
* aria-label="Settings"
|
|
44
|
+
* icon={<SettingsIcon />}
|
|
45
|
+
* label="Settings"
|
|
46
|
+
* variant="outline"
|
|
47
|
+
* />
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Labelled by external element
|
|
51
|
+
* <span id="btn-label">Delete item</span>
|
|
52
|
+
* <IconButton type="button" aria-labelledby="btn-label" icon={<TrashIcon />} />
|
|
53
|
+
*/
|
|
54
|
+
export const IconButton = ({
|
|
55
|
+
icon,
|
|
56
|
+
label,
|
|
57
|
+
variant = "icon",
|
|
58
|
+
type = "button",
|
|
59
|
+
...props
|
|
60
|
+
}: IconButtonProps) => (
|
|
61
|
+
<Button
|
|
62
|
+
variant={variant}
|
|
63
|
+
data-icon-btn={label ? "has-label" : "icon"}
|
|
64
|
+
{...props}
|
|
65
|
+
type={type}
|
|
66
|
+
>
|
|
67
|
+
{icon}
|
|
68
|
+
{label && <span data-icon-label>{label}</span>}
|
|
69
|
+
</Button>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
IconButton.displayName = "IconButton";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import UI from
|
|
2
|
-
import React from
|
|
3
|
-
import { useDisabledState } from
|
|
1
|
+
import UI from "../ui";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { useDisabledState } from "../../hooks/use-disabled-state";
|
|
4
4
|
|
|
5
|
-
export type { SelectProps } from
|
|
6
|
-
import type { SelectProps } from
|
|
5
|
+
export type { SelectProps } from "./form.types";
|
|
6
|
+
import type { SelectProps } from "./form.types";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Option component props interface
|
|
@@ -11,56 +11,57 @@ import type { SelectProps } from './form.types'
|
|
|
11
11
|
*
|
|
12
12
|
* @interface OptionProps
|
|
13
13
|
*/
|
|
14
|
-
export interface OptionProps
|
|
14
|
+
export interface OptionProps
|
|
15
|
+
extends Omit<React.ComponentPropsWithoutRef<"option">, "className"> {
|
|
15
16
|
/**
|
|
16
17
|
* Value for the select option (required, unless using legacy selectValue)
|
|
17
18
|
*/
|
|
18
|
-
value?: string | number
|
|
19
|
+
value?: string | number;
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Display label for the option (defaults to value if not provided)
|
|
22
23
|
*/
|
|
23
|
-
label?: string
|
|
24
|
+
label?: string;
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* CSS class names (preferred over 'className' for consistency with fpkit components)
|
|
27
28
|
*/
|
|
28
|
-
classes?: string
|
|
29
|
+
classes?: string;
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Inline CSS styles object
|
|
32
33
|
*/
|
|
33
|
-
styles?: React.CSSProperties
|
|
34
|
+
styles?: React.CSSProperties;
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Disabled state for the option
|
|
37
38
|
* @default false
|
|
38
39
|
*/
|
|
39
|
-
disabled?: boolean
|
|
40
|
+
disabled?: boolean;
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Children content (overrides label if provided)
|
|
43
44
|
*/
|
|
44
|
-
children?: React.ReactNode
|
|
45
|
+
children?: React.ReactNode;
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Visual variant for styling via data-option attribute
|
|
48
49
|
* Use with CSS: option[data-option="primary"] { ... }
|
|
49
50
|
* @example 'primary' | 'secondary' | 'success' | 'error'
|
|
50
51
|
*/
|
|
51
|
-
variant?: string
|
|
52
|
+
variant?: string;
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Size variant for styling via data-size attribute
|
|
55
56
|
* @example 'sm' | 'md' | 'lg'
|
|
56
57
|
*/
|
|
57
|
-
size?: string
|
|
58
|
+
size?: string;
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* Additional data attributes for custom styling
|
|
61
62
|
* @example { 'data-highlighted': true, 'data-category': 'premium' }
|
|
62
63
|
*/
|
|
63
|
-
dataAttributes?: Record<string, string | boolean | number
|
|
64
|
+
dataAttributes?: Record<string, string | boolean | number>;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/**
|
|
@@ -88,7 +89,10 @@ export interface OptionProps extends Omit<React.ComponentPropsWithoutRef<'option
|
|
|
88
89
|
* @param {OptionProps} props - Component props
|
|
89
90
|
* @returns {JSX.Element} Option element
|
|
90
91
|
*/
|
|
91
|
-
export const Option = React.forwardRef<
|
|
92
|
+
export const Option = React.forwardRef<
|
|
93
|
+
HTMLOptionElement,
|
|
94
|
+
OptionProps & Partial<SelectOptionsProps>
|
|
95
|
+
>(
|
|
92
96
|
(
|
|
93
97
|
{
|
|
94
98
|
value,
|
|
@@ -105,18 +109,18 @@ export const Option = React.forwardRef<HTMLOptionElement, OptionProps & Partial<
|
|
|
105
109
|
selectLabel,
|
|
106
110
|
...props
|
|
107
111
|
},
|
|
108
|
-
ref
|
|
112
|
+
ref,
|
|
109
113
|
) => {
|
|
110
114
|
// Map legacy props to new props
|
|
111
|
-
const optionValue = value ?? selectValue
|
|
112
|
-
const optionLabel = label ?? selectLabel
|
|
115
|
+
const optionValue = value ?? selectValue;
|
|
116
|
+
const optionLabel = label ?? selectLabel;
|
|
113
117
|
|
|
114
118
|
// Build data attributes object for styling
|
|
115
119
|
const combinedDataAttrs = {
|
|
116
|
-
...(variant && {
|
|
117
|
-
...(size && {
|
|
120
|
+
...(variant && { "data-option": variant }),
|
|
121
|
+
...(size && { "data-size": size }),
|
|
118
122
|
...dataAttributes,
|
|
119
|
-
}
|
|
123
|
+
};
|
|
120
124
|
|
|
121
125
|
return (
|
|
122
126
|
<UI
|
|
@@ -131,17 +135,17 @@ export const Option = React.forwardRef<HTMLOptionElement, OptionProps & Partial<
|
|
|
131
135
|
>
|
|
132
136
|
{children || optionLabel || optionValue}
|
|
133
137
|
</UI>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
)
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
);
|
|
137
141
|
|
|
138
|
-
Option.displayName =
|
|
142
|
+
Option.displayName = "Select.Option";
|
|
139
143
|
|
|
140
144
|
// Legacy type export for backwards compatibility
|
|
141
|
-
export type SelectOptionsProps = Omit<OptionProps,
|
|
142
|
-
selectValue: string | number
|
|
143
|
-
selectLabel?: string
|
|
144
|
-
}
|
|
145
|
+
export type SelectOptionsProps = Omit<OptionProps, "classes" | "styles"> & {
|
|
146
|
+
selectValue: string | number;
|
|
147
|
+
selectLabel?: string;
|
|
148
|
+
};
|
|
145
149
|
|
|
146
150
|
/**
|
|
147
151
|
* Select component - Accessible dropdown selection input with validation support
|
|
@@ -187,7 +191,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
|
187
191
|
children,
|
|
188
192
|
required,
|
|
189
193
|
selected,
|
|
190
|
-
validationState =
|
|
194
|
+
validationState = "none",
|
|
191
195
|
errorMessage,
|
|
192
196
|
hintText,
|
|
193
197
|
onBlur,
|
|
@@ -197,7 +201,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
|
197
201
|
onEnter,
|
|
198
202
|
...props
|
|
199
203
|
},
|
|
200
|
-
ref
|
|
204
|
+
ref,
|
|
201
205
|
) => {
|
|
202
206
|
// Use the disabled state hook with enhanced API for automatic className merging
|
|
203
207
|
const { disabledProps, handlers } = useDisabledState<HTMLSelectElement>(
|
|
@@ -210,33 +214,33 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
|
210
214
|
onKeyDown: (e: React.KeyboardEvent<HTMLSelectElement>) => {
|
|
211
215
|
// Handle Enter key press for accessibility
|
|
212
216
|
// Enables keyboard-only users to trigger actions after selection
|
|
213
|
-
if (e.key ===
|
|
214
|
-
onEnter(e)
|
|
217
|
+
if (e.key === "Enter" && onEnter) {
|
|
218
|
+
onEnter(e);
|
|
215
219
|
}
|
|
216
220
|
// Always call consumer's onKeyDown if provided
|
|
217
221
|
if (onKeyDown) {
|
|
218
|
-
onKeyDown(e)
|
|
222
|
+
onKeyDown(e);
|
|
219
223
|
}
|
|
220
224
|
},
|
|
221
225
|
},
|
|
222
226
|
// Automatic className merging - hook combines disabled class with user classes
|
|
223
227
|
className: classes,
|
|
224
|
-
}
|
|
225
|
-
)
|
|
228
|
+
},
|
|
229
|
+
);
|
|
226
230
|
|
|
227
231
|
// Determine aria-invalid based on validation state
|
|
228
|
-
const isInvalid = validationState ===
|
|
232
|
+
const isInvalid = validationState === "invalid";
|
|
229
233
|
|
|
230
234
|
// Generate describedby IDs for error and hint text
|
|
231
|
-
const describedByIds: string[] = []
|
|
235
|
+
const describedByIds: string[] = [];
|
|
232
236
|
if (errorMessage && id) {
|
|
233
|
-
describedByIds.push(`${id}-error`)
|
|
237
|
+
describedByIds.push(`${id}-error`);
|
|
234
238
|
}
|
|
235
239
|
if (hintText && id) {
|
|
236
|
-
describedByIds.push(`${id}-hint`)
|
|
240
|
+
describedByIds.push(`${id}-hint`);
|
|
237
241
|
}
|
|
238
242
|
const ariaDescribedBy =
|
|
239
|
-
describedByIds.length > 0 ? describedByIds.join(
|
|
243
|
+
describedByIds.length > 0 ? describedByIds.join(" ") : undefined;
|
|
240
244
|
|
|
241
245
|
return (
|
|
242
246
|
<UI
|
|
@@ -249,7 +253,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
|
249
253
|
{...handlers}
|
|
250
254
|
required={required}
|
|
251
255
|
aria-required={required}
|
|
252
|
-
aria-disabled={disabledProps[
|
|
256
|
+
aria-disabled={disabledProps["aria-disabled"]}
|
|
253
257
|
aria-invalid={isInvalid}
|
|
254
258
|
aria-describedby={ariaDescribedBy}
|
|
255
259
|
style={styles}
|
|
@@ -257,19 +261,19 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
|
257
261
|
>
|
|
258
262
|
{children || <option value="" />}
|
|
259
263
|
</UI>
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
)
|
|
264
|
+
);
|
|
265
|
+
},
|
|
266
|
+
);
|
|
263
267
|
|
|
264
|
-
Select.displayName =
|
|
268
|
+
Select.displayName = "Select";
|
|
265
269
|
|
|
266
270
|
// Create a compound component with proper typing
|
|
267
271
|
type SelectComponent = typeof Select & {
|
|
268
|
-
Option: typeof Option
|
|
269
|
-
}
|
|
272
|
+
Option: typeof Option;
|
|
273
|
+
};
|
|
270
274
|
|
|
271
275
|
// Type assertion to allow adding static property to ForwardRefExoticComponent
|
|
272
276
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
273
|
-
|
|
277
|
+
(Select as any).Option = Option;
|
|
274
278
|
|
|
275
|
-
export default Select as SelectComponent
|
|
279
|
+
export default Select as SelectComponent;
|