@allxsmith/bestax-bulma 5.15.0 → 5.15.2

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.
@@ -1,12 +1,26 @@
1
1
  import React from 'react';
2
+ import type { PolymorphicComponent } from '../helpers/polymorphic';
2
3
  import { BulmaClassesProps, validColors } from '../helpers/useBulmaClasses';
3
4
  /**
4
- * Props for the Link component.
5
- * @extraProp {string} [href] - The URL the link points to.
6
- * @extraProp {'_self' | '_blank' | '_parent' | '_top'} [target] - Where to open the linked document.
7
- * @extraProp {string} [rel] - Relationship between current and linked document.
5
+ * The Link component's own props — everything it adds on top of the attributes
6
+ * of whatever element `as` renders.
8
7
  */
9
- export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>, Omit<BulmaClassesProps, 'color' | 'backgroundColor'> {
8
+ export interface LinkOwnProps extends Omit<BulmaClassesProps, 'color' | 'backgroundColor'> {
9
+ /**
10
+ * Not accepted. `color` on this component would be the deprecated
11
+ * presentational HTML attribute, and `useBulmaClasses` consumes any `color`
12
+ * key as a Bulma helper before the target could see it — so it is declared
13
+ * unavailable rather than silently eaten. Use `textColor` / `bgColor`.
14
+ * @internal
15
+ */
16
+ color?: never;
17
+ /**
18
+ * Not accepted under this name. `useBulmaClasses` consumes any
19
+ * `backgroundColor` key before `rest` is spread, so a custom `as` target
20
+ * declaring one would never receive it. Use `bgColor`.
21
+ * @internal
22
+ */
23
+ backgroundColor?: never;
10
24
  /** Additional CSS classes to apply. */
11
25
  className?: string;
12
26
  /** Text color helper. */
@@ -15,17 +29,27 @@ export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>
15
29
  bgColor?: (typeof validColors)[number] | 'inherit' | 'current';
16
30
  /** Whether the link appears active. */
17
31
  isActive?: boolean;
18
- /** Render as a custom component (e.g. a router `Link`) instead of `<a>`. Defaults to `'a'`. */
19
- as?: React.ElementType;
20
32
  /** Content to render inside the link. */
21
33
  children?: React.ReactNode;
22
34
  }
35
+ /**
36
+ * Props for the Link component. The DOM attributes and the `ref` both follow
37
+ * `as`: with the default `'a'` the anchor attributes (`href`, `target`, `rel`)
38
+ * are accepted, with `as="span"` they are not.
39
+ *
40
+ * @extraProp {PolymorphicRef<React.ElementType>} [ref] - Ref forwarded to the element `as` renders, typed from `as`: the DOM node for an intrinsic tag, or whatever handle a custom component exposes.
41
+ */
42
+ export type LinkProps<T extends React.ElementType = 'a'> = LinkOwnProps & Omit<React.ComponentPropsWithoutRef<T>, keyof LinkOwnProps | 'as'> & {
43
+ /** Render as another intrinsic element (`'span'`, `'button'`) or a custom component (e.g. a router `Link`) instead of `<a>`. Defaults to `'a'`. */
44
+ as?: T;
45
+ };
23
46
  /**
24
47
  * The `Link` component renders a styled anchor (`<a>`) element with Bulma helper class integration.
25
48
  *
26
49
  * @function
27
50
  * @param {LinkProps} props - Props for the Link component.
28
- * @returns {JSX.Element} The rendered anchor element.
51
+ * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
52
+ * @returns {JSX.Element} The rendered anchor, or whatever `as` names.
29
53
  * @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
30
54
  */
31
- export declare const Link: React.FC<LinkProps>;
55
+ export declare const Link: PolymorphicComponent<LinkOwnProps, "a">;
@@ -1,19 +1,48 @@
1
- import { ButtonProps } from './Button';
1
+ import React from 'react';
2
+ import { ButtonOwnProps } from './Button';
3
+ import type { PolymorphicComponent } from '../helpers/polymorphic';
2
4
  /**
3
- * Props for the LinkButton component.
5
+ * The LinkButton component's own props — everything it adds on top of the
6
+ * attributes of whatever element `as` renders.
4
7
  */
5
- export interface LinkButtonProps extends Omit<ButtonProps, 'color' | 'isOutlined' | 'isInverted' | 'isLight'> {
8
+ export interface LinkButtonOwnProps extends Omit<ButtonOwnProps, 'color' | 'isOutlined' | 'isInverted' | 'isLight'> {
6
9
  /** Display mode. `text` has no underline and highlights its background on hover; `ghost` uses the default text color and underlines on hover; `underline` drops the button chrome entirely (transparent background and border) and underlines on hover or focus. */
7
10
  variant?: 'text' | 'ghost' | 'underline';
8
11
  /** Text color override for the button. */
9
12
  color?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger' | 'white' | 'light' | 'dark' | 'black';
13
+ /**
14
+ * Not available on `LinkButton`, and declared so that a custom `as` target
15
+ * cannot reintroduce it. `Button` consumes these three for styling variants
16
+ * `LinkButton` does not offer, and strips them before rendering the target —
17
+ * so without this a component requiring one would type-check and silently
18
+ * never receive it.
19
+ * @internal
20
+ */
21
+ isOutlined?: never;
22
+ /** @internal Not available on `LinkButton` — see `isOutlined`. */
23
+ isInverted?: never;
24
+ /** @internal Not available on `LinkButton` — see `isOutlined`. */
25
+ isLight?: never;
10
26
  }
27
+ /**
28
+ * Props for the LinkButton component. The DOM attributes and the `ref` both
29
+ * follow `as`, exactly as they do on `Button`.
30
+ *
31
+ * @extraProp {PolymorphicRef<React.ElementType>} [ref] - Ref forwarded to the element `as` renders, typed from `as`: the DOM node for an intrinsic tag, or whatever handle a custom component exposes.
32
+ */
33
+ export type LinkButtonProps<T extends React.ElementType = 'button'> = LinkButtonOwnProps & Omit<React.ComponentPropsWithoutRef<T>, keyof LinkButtonOwnProps | 'as'> & {
34
+ /**
35
+ * Render as a `<button>`, `<a>`, or a custom component (e.g. a router `Link`).
36
+ * @defaultValue 'button'
37
+ */
38
+ as?: T;
39
+ };
11
40
  /**
12
41
  * The `LinkButton` component renders a `<button>` that visually looks like text or a link.
13
42
  *
14
43
  * @function
15
44
  * @param {LinkButtonProps} props - Props for the LinkButton component.
16
- * @param {React.Ref<HTMLButtonElement | HTMLAnchorElement>} ref - Forwarded ref to the rendered button or anchor element.
45
+ * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
17
46
  * @returns {JSX.Element} The rendered link-styled button element.
18
47
  *
19
48
  * @example
@@ -24,5 +53,5 @@ export interface LinkButtonProps extends Omit<ButtonProps, 'color' | 'isOutlined
24
53
  * // Underline variant with color
25
54
  * <LinkButton variant="underline" color="primary">Learn more</LinkButton>
26
55
  */
27
- export declare const LinkButton: import("react").ForwardRefExoticComponent<LinkButtonProps & import("react").RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
56
+ export declare const LinkButton: PolymorphicComponent<LinkButtonOwnProps, "button">;
28
57
  export default LinkButton;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import type { IconProps } from './Icon';
3
+ /**
4
+ * Distinguishes an `IconProps` object from a plain custom node (an inline SVG, a
5
+ * `react-icons` component, …) in a slot that accepts either — `Control`'s
6
+ * `iconLeft`/`iconRight`, `IconText`'s `iconProps`.
7
+ *
8
+ * One copy, imported by both consumers: two hand-maintained copies, one per
9
+ * consumer, is how the deprecated `icon` path came to crash both (#663).
10
+ *
11
+ * Its own module rather than `Icon.tsx` because `src/index.ts` wildcard-exports
12
+ * that file, and everything published there is public forever. `polymorphic.ts`
13
+ * makes the same call for `isCustomElement`: an internal discriminator is not
14
+ * something to support for the life of the package.
15
+ *
16
+ * It tests each member's VALUE, not merely its key, and only after everything
17
+ * React owns is out of the way. `'icon' in value` was enough
18
+ * to claim a Font Awesome `IconDefinition` — `{ prefix, iconName, icon: [w, h, …,
19
+ * path] }` — whose `icon` is a path array rather than a class string. That object
20
+ * is not a renderable node either, so the honest outcome is the one this slot has
21
+ * always had for it: the node branch, and React's own "Objects are not valid as a
22
+ * React child". Claiming it here instead rendered a blank container that still
23
+ * reserved layout.
24
+ *
25
+ * A member added to `IconProps` needs a line here, and a missed one is a crash
26
+ * rather than a fallthrough. That is the cost of a slot taking either a props
27
+ * object or a node; keeping the guard in one place is what keeps the cost to a
28
+ * single line.
29
+ */
30
+ export declare function isIconProps(value: IconProps | React.ReactNode): value is IconProps;
@@ -0,0 +1,138 @@
1
+ import type React from 'react';
2
+ /**
3
+ * Whether an `as` target is a custom element rather than a built-in tag.
4
+ *
5
+ * HTML requires a custom element's name to contain a hyphen, and no built-in
6
+ * element name has one, so this is exact rather than a heuristic. It matters
7
+ * because a custom element is an intrinsic STRING — `typeof as === 'string'` is
8
+ * true — while its props are whatever a consumer declared through
9
+ * `React.JSX.IntrinsicElements`, the way this package declares `<ion-icon>`.
10
+ * A runtime backstop that filters built-in attributes must leave those alone,
11
+ * or it strips props the derived type just promised to forward.
12
+ */
13
+ export declare function isCustomElement(as: unknown): as is string;
14
+ /**
15
+ * The ref type of whatever element `as` renders.
16
+ *
17
+ * Derived from `ComponentPropsWithRef` rather than `React.ComponentRef` so it
18
+ * resolves on every `@types/react` a consumer may be on: this package supports
19
+ * React 18 and 19, and the CI matrix pins only `react`/`react-dom`, never the
20
+ * types, so a types-only regression here would not be caught.
21
+ *
22
+ * `never` when the target takes no ref. A plain function component has no `ref`
23
+ * key at all, and React's own types reject `<PlainFC ref={…} />`; without the
24
+ * guard the indexed access degraded to `unknown` and we accepted a ref that
25
+ * silently does nothing — `ref.current` stays null and a later `.focus()`
26
+ * throws. Matching React's strictness is the point.
27
+ */
28
+ export type PolymorphicRef<T extends React.ElementType> = 'ref' extends keyof React.ComponentPropsWithRef<T> ? React.ComponentPropsWithRef<T>['ref'] : never;
29
+ /**
30
+ * A component's own props, plus the attributes of the element `as` names.
31
+ *
32
+ * `Own` wins every collision — a component that declares `color` as a Bulma
33
+ * variant keeps it, rather than inheriting the DOM attribute of the same name.
34
+ * That subtraction is the ONLY one. `color` used to be dropped here as well, to
35
+ * stop the deprecated presentational HTML attribute reaching the three
36
+ * components whose own props do not declare one — but doing it in the shared
37
+ * type also stripped `color` from a CUSTOM target that legitimately has one.
38
+ * Those three declare `color?: never` themselves instead, which lands in
39
+ * `keyof Own` and reaches the same result without a special case here.
40
+ *
41
+ * Distributive over `T` on purpose. `Omit<A | B, K>` keys off `keyof (A | B)`,
42
+ * which is only what A and B share — so a union-typed `as` (a ternary, or a
43
+ * variable typed `'a' | 'button'`) silently lost every prop that belongs to
44
+ * just one member, `href` among them. Distributing produces a union of prop
45
+ * shapes instead, and each member keeps its own.
46
+ *
47
+ * Each component writes this intersection out in its own `*Props` alias rather
48
+ * than referring to this type: the API-docs extractor
49
+ * (`scripts/lib/props-extract.mjs`) walks heritage syntactically, and a generic
50
+ * alias leaves it resolving a type parameter it cannot see through.
51
+ * This type is here for the component's cast target and for consumers writing
52
+ * wrappers.
53
+ */
54
+ export type PolymorphicProps<T extends React.ElementType, Own> = T extends unknown ? Own & Omit<React.ComponentPropsWithoutRef<T>, keyof Own | 'as'> & {
55
+ /** The element or component to render. */
56
+ as?: T;
57
+ } : never;
58
+ /**
59
+ * A component whose props and forwarded ref both follow `as`.
60
+ *
61
+ * `forwardRef` cannot express a generic component, so the implementation is
62
+ * cast to this. `displayName` is part of the type because the library sets it
63
+ * on every `forwardRef` component, and `withSubComponents` constrains its base
64
+ * to `{ displayName?: string }`.
65
+ *
66
+ * The second, non-generic signature exists for type DERIVATION rather than for
67
+ * calls. `React.ComponentProps<typeof Button>` infers from the last overload;
68
+ * with only the generic one it instantiated at the constraint and collapsed to
69
+ * `any`, so a consumer building their own prop type on ours lost every check.
70
+ * Calls still resolve against the generic signature first, so ordering here is
71
+ * load-bearing — swapping the two puts the collapse back.
72
+ *
73
+ * **A wrapping HOC erases the genericity.** `React.memo(Button)`,
74
+ * `React.lazy`, a `styled()` wrapper — anything that infers its props through
75
+ * `ComponentProps<T>` — instantiates the type parameter once and hands back a
76
+ * component with a single prop type. It takes the LAST call signature, which is
77
+ * the derivation overload, so the wrapper is pinned to the DEFAULT element:
78
+ * `React.memo(Button)` rejects `as="div"` outright, and `React.memo(Dropdown.Item)`
79
+ * renders neither a `<div>` nor a `<button>`, though both components accept those
80
+ * directly. Stricter, not looser — the checks do not stop, they collapse onto one
81
+ * element, and a `Dropdown.Item` memoized for a long menu list is the case that
82
+ * meets it. This is inherent to polymorphic components in TypeScript, not
83
+ * something this library can fix. Re-assert the type to get the rest back:
84
+ *
85
+ * ```tsx
86
+ * const MemoButton = React.memo(Button) as typeof Button;
87
+ * ```
88
+ */
89
+ export interface PolymorphicComponent<Own, Default extends React.ElementType> {
90
+ <T extends React.ElementType = Default>(props: PolymorphicProps<T, Own> & {
91
+ ref?: PolymorphicRef<T>;
92
+ }): React.ReactElement | null;
93
+ (props: PolymorphicProps<Default, Own> & {
94
+ ref?: PolymorphicRef<Default>;
95
+ }): React.ReactElement | null;
96
+ displayName?: string;
97
+ }
98
+ /**
99
+ * A component whose props follow `as` but which forwards no ref.
100
+ *
101
+ * For components that own the node they observe — `Reveal` keeps its own ref on
102
+ * the element it watches for scroll intersection, which is not always the
103
+ * element `as` names.
104
+ */
105
+ export interface PolymorphicComponentWithoutRef<Own, Default extends React.ElementType> {
106
+ <T extends React.ElementType = Default>(props: PolymorphicProps<T, Own>): React.ReactElement | null;
107
+ (props: PolymorphicProps<Default, Own>): React.ReactElement | null;
108
+ displayName?: string;
109
+ }
110
+ /**
111
+ * A component whose props follow `as`, where `as` is a closed set of tags.
112
+ *
113
+ * `PolymorphicComponentWithoutRef` lets `as` name anything; this narrows it to
114
+ * `Allowed`. Bulma pins some elements by its own markup contract — a dropdown
115
+ * item is an `<a>`, a `<div>` or a `<button>` and nothing else — and the
116
+ * constraint keeps that promise while still deriving props from whichever of
117
+ * the three a caller picks.
118
+ *
119
+ * Genericity is what makes the derivation exact for a LITERAL `as`, and it is
120
+ * worth the extra type parameter. Typing such a component as `React.FC<Union>`
121
+ * instead — a union of the three prop shapes — reads as equivalent but is not:
122
+ * against a union target, an object literal's excess-property check passes if
123
+ * the property exists in ANY member, so `<Item as="div" href="/x" />` slips
124
+ * through. Inferring `T` from the literal checks against that one member.
125
+ *
126
+ * A UNION-typed `as` — a ternary, or a variable typed as the whole set — infers
127
+ * `T` as the union and lands back in that same permissive check, so
128
+ * `<Item as={tag} href="/x" />` compiles whatever `tag` turns out to be. That is
129
+ * the open `as` behaviour too, and deliberate there: `PolymorphicProps`
130
+ * distributes precisely so a union-typed `as` KEEPS each member's props rather
131
+ * than losing every prop the members do not share. Narrowing it is not a
132
+ * constrained-component question; it would have to change for `Button` first.
133
+ */
134
+ export interface ConstrainedPolymorphicComponentWithoutRef<Own, Allowed extends React.ElementType, Default extends Allowed> {
135
+ <T extends Allowed = Default>(props: PolymorphicProps<T, Own>): React.ReactElement | null;
136
+ (props: PolymorphicProps<Default, Own>): React.ReactElement | null;
137
+ displayName?: string;
138
+ }
@@ -100,6 +100,7 @@ export type { FormFieldProps } from './form/fieldProps';
100
100
  export * from './grid/Cell';
101
101
  export * from './grid/Grid';
102
102
  export * from './helpers/classNames';
103
+ export type { ConstrainedPolymorphicComponentWithoutRef, PolymorphicComponent, PolymorphicComponentWithoutRef, PolymorphicProps, PolymorphicRef, } from './helpers/polymorphic';
103
104
  export * from './helpers/mergeBulmaStyles';
104
105
  export * from './helpers/useBulmaClasses';
105
106
  export * from './helpers/Theme';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allxsmith/bestax-bulma",
3
- "version": "5.15.0",
3
+ "version": "5.15.2",
4
4
  "description": "A fully-typed React component library for the Bulma CSS framework. Build modern UIs quickly with reusable, accessible, and customizable Bulma-based React components.",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",
@@ -190,6 +190,7 @@
190
190
  "bundle:stats": "rollup -c --configPlugin visualizer",
191
191
  "lint": "eslint src",
192
192
  "typecheck": "tsc --noEmit",
193
+ "typecheck:tests": "tsc -p tsconfig.test.json --noEmit",
193
194
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
194
195
  "format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
195
196
  "clean": "rimraf dist",
@@ -63,6 +63,6 @@ Rules the pattern encodes:
63
63
  prefix together: name your variables `<your-prefix>-*` or the matcher assigns them to
64
64
  nobody. After adding one, run `pnpm gen:api-sources` and confirm the component's
65
65
  `SCSS_SOURCES` entry is non-empty — an empty entry silently suppresses the API page's
66
- CSS & Sass Variables section, which is how LinkButton shipped four variables invisibly
66
+ CSS & Sass Variables section, which is how LinkButton shipped its variables invisibly
67
67
  (#464). The orphan rule in `check:conformance` now fails on a partial that no component
68
68
  claims, so the miss is loud, but the fix is still yours to make.