@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.
Files changed (116) hide show
  1. package/libs/chunk-25KCUE3R.cjs +17 -0
  2. package/libs/chunk-25KCUE3R.cjs.map +1 -0
  3. package/libs/chunk-34NWHFHP.js +10 -0
  4. package/libs/chunk-34NWHFHP.js.map +1 -0
  5. package/libs/{chunk-SQ44OCJ2.js → chunk-6NMLU5FA.js} +2 -2
  6. package/libs/{chunk-GVVCXXKI.cjs → chunk-6YVR4TDM.cjs} +3 -3
  7. package/libs/chunk-DSQ2TUCR.js +7 -0
  8. package/libs/chunk-DSQ2TUCR.js.map +1 -0
  9. package/libs/{chunk-H6A2CUWA.js → chunk-VQTCTLFN.js} +2 -2
  10. package/libs/chunk-ZJ4RUKI2.cjs +14 -0
  11. package/libs/chunk-ZJ4RUKI2.cjs.map +1 -0
  12. package/libs/{chunk-H4JRUNKU.cjs → chunk-ZOPHCNFD.cjs} +3 -3
  13. package/libs/components/button.cjs +3 -3
  14. package/libs/components/button.d.cts +34 -1
  15. package/libs/components/button.d.ts +34 -1
  16. package/libs/components/button.js +1 -1
  17. package/libs/components/buttons/button.css +1 -1
  18. package/libs/components/buttons/button.css.map +1 -1
  19. package/libs/components/buttons/button.min.css +2 -2
  20. package/libs/components/buttons/icon-button.css +1 -0
  21. package/libs/components/buttons/icon-button.css.map +1 -0
  22. package/libs/components/buttons/icon-button.min.css +3 -0
  23. package/libs/components/dialog/dialog.cjs +4 -4
  24. package/libs/components/dialog/dialog.js +2 -2
  25. package/libs/components/icons/icon.d.cts +1 -1
  26. package/libs/components/icons/icon.d.ts +1 -1
  27. package/libs/components/layout/landmarks.css +1 -1
  28. package/libs/components/layout/landmarks.css.map +1 -1
  29. package/libs/components/layout/landmarks.min.css +2 -2
  30. package/libs/components/link/link.css +1 -1
  31. package/libs/components/link/link.min.css +1 -1
  32. package/libs/components/modal.cjs +3 -3
  33. package/libs/components/modal.js +2 -2
  34. package/libs/components/popover/popover.cjs +3 -8
  35. package/libs/components/popover/popover.css +1 -0
  36. package/libs/components/popover/popover.css.map +1 -0
  37. package/libs/components/popover/popover.d.cts +54 -26
  38. package/libs/components/popover/popover.d.ts +54 -26
  39. package/libs/components/popover/popover.js +1 -2
  40. package/libs/components/popover/popover.min.css +3 -0
  41. package/libs/hooks.cjs +3 -6
  42. package/libs/hooks.cjs.map +1 -1
  43. package/libs/hooks.d.cts +30 -10
  44. package/libs/hooks.d.ts +30 -10
  45. package/libs/hooks.js +5 -1
  46. package/libs/hooks.js.map +1 -1
  47. package/libs/{icons-48788561.d.ts → icons-2c09535c.d.ts} +32 -32
  48. package/libs/icons.d.cts +1 -1
  49. package/libs/icons.d.ts +1 -1
  50. package/libs/index.cjs +41 -40
  51. package/libs/index.cjs.map +1 -1
  52. package/libs/index.css +1 -1
  53. package/libs/index.css.map +1 -1
  54. package/libs/index.d.cts +101 -5
  55. package/libs/index.d.ts +101 -5
  56. package/libs/index.js +14 -15
  57. package/libs/index.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/components/buttons/README.mdx +107 -11
  60. package/src/components/buttons/STYLES.mdx +182 -47
  61. package/src/components/buttons/button.scss +93 -16
  62. package/src/components/buttons/button.stories.tsx +149 -0
  63. package/src/components/buttons/button.test.tsx +12 -0
  64. package/src/components/buttons/button.tsx +50 -6
  65. package/src/components/buttons/icon-button.scss +45 -0
  66. package/src/components/buttons/icon-button.stories.tsx +200 -0
  67. package/src/components/buttons/icon-button.test.tsx +132 -0
  68. package/src/components/buttons/icon-button.tsx +72 -0
  69. package/src/components/form/select.tsx +55 -51
  70. package/src/components/layout/README.mdx +1117 -0
  71. package/src/components/layout/STYLES.mdx +159 -4
  72. package/src/components/layout/fieldset.stories.tsx +387 -0
  73. package/src/components/layout/landmarks.scss +115 -2
  74. package/src/components/layout/landmarks.stories.tsx +2 -6
  75. package/src/components/layout/landmarks.tsx +96 -27
  76. package/src/components/link/link.scss +2 -2
  77. package/src/components/popover/README.mdx +478 -0
  78. package/src/components/popover/STYLES.mdx +389 -0
  79. package/src/components/popover/index.ts +3 -0
  80. package/src/components/popover/popover.scss +249 -0
  81. package/src/components/popover/popover.stories.tsx +315 -15
  82. package/src/components/popover/popover.test.tsx +249 -37
  83. package/src/components/popover/popover.tsx +165 -62
  84. package/src/hooks/popover/popover.tsx +26 -10
  85. package/src/hooks/popover/use-popover.tsx +30 -10
  86. package/src/hooks.ts +5 -0
  87. package/src/index.scss +1 -0
  88. package/src/index.ts +1 -0
  89. package/src/styles/buttons/button.css +78 -16
  90. package/src/styles/buttons/button.css.map +1 -1
  91. package/src/styles/buttons/icon-button.css +32 -0
  92. package/src/styles/buttons/icon-button.css.map +1 -0
  93. package/src/styles/index.css +350 -18
  94. package/src/styles/index.css.map +1 -1
  95. package/src/styles/layout/landmarks.css +83 -0
  96. package/src/styles/layout/landmarks.css.map +1 -1
  97. package/src/styles/link/link.css +2 -2
  98. package/src/styles/popover/popover.css +190 -0
  99. package/src/styles/popover/popover.css.map +1 -0
  100. package/src/types/popover.d.ts +64 -0
  101. package/libs/chunk-4I5MF54P.js +0 -8
  102. package/libs/chunk-4I5MF54P.js.map +0 -1
  103. package/libs/chunk-GCGKYLDG.js +0 -7
  104. package/libs/chunk-GCGKYLDG.js.map +0 -1
  105. package/libs/chunk-NZVSXRTB.cjs +0 -16
  106. package/libs/chunk-NZVSXRTB.cjs.map +0 -1
  107. package/libs/chunk-PDD4N5P5.cjs +0 -10
  108. package/libs/chunk-PDD4N5P5.cjs.map +0 -1
  109. package/libs/chunk-S7NIA6PI.cjs +0 -17
  110. package/libs/chunk-S7NIA6PI.cjs.map +0 -1
  111. package/libs/chunk-X2RDXWH5.js +0 -10
  112. package/libs/chunk-X2RDXWH5.js.map +0 -1
  113. /package/libs/{chunk-SQ44OCJ2.js.map → chunk-6NMLU5FA.js.map} +0 -0
  114. /package/libs/{chunk-GVVCXXKI.cjs.map → chunk-6YVR4TDM.cjs.map} +0 -0
  115. /package/libs/{chunk-H6A2CUWA.js.map → chunk-VQTCTLFN.js.map} +0 -0
  116. /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 '../ui'
2
- import React from 'react'
3
- import { useDisabledState } from '../../hooks/use-disabled-state'
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 './form.types'
6
- import type { SelectProps } from './form.types'
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 extends Omit<React.ComponentPropsWithoutRef<'option'>, 'className'> {
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<HTMLOptionElement, OptionProps & Partial<SelectOptionsProps>>(
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 && { 'data-option': variant }),
117
- ...(size && { 'data-size': 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 = 'Select.Option'
142
+ Option.displayName = "Select.Option";
139
143
 
140
144
  // Legacy type export for backwards compatibility
141
- export type SelectOptionsProps = Omit<OptionProps, 'classes' | 'styles'> & {
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 = 'none',
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 === 'Enter' && onEnter) {
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 === 'invalid'
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(' ') : undefined
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['aria-disabled']}
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 = 'Select'
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
- ;(Select as any).Option = Option
277
+ (Select as any).Option = Option;
274
278
 
275
- export default Select as SelectComponent
279
+ export default Select as SelectComponent;