@ideasui/button 0.0.2-beta.1 → 0.0.2-canary.1786301183157
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 +15 -13
- package/dist/index.mjs +2 -2
- package/package.json +15 -12
- package/src/button-group-context.tsx +7 -6
- package/src/button-group.tsx +49 -27
- package/src/button.tsx +112 -105
- package/src/button.types.ts +81 -26
package/README.md
CHANGED
|
@@ -56,19 +56,20 @@ function ActionMenu() {
|
|
|
56
56
|
|
|
57
57
|
### Button Props
|
|
58
58
|
|
|
59
|
-
| Prop | Type | Default | Description
|
|
60
|
-
| :----------- | :------------------------------------------------- | :---------- |
|
|
61
|
-
| `variant` | `'solid' \| 'outline' \| 'ghost'` | `'solid'` | Visual style variant.
|
|
62
|
-
| `color` | `SemanticColor` | `'default'` | Semantic color intent (primary, success, etc).
|
|
63
|
-
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | The size of the button.
|
|
64
|
-
| `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | The corner radius.
|
|
65
|
-
| `loading` | `boolean` | `false` | Shows a spinner and disables interaction.
|
|
66
|
-
| `disabled` | `boolean` | `false` | Disables the button.
|
|
67
|
-
| `fullWidth` | `boolean` | `false` | Expands to fill the container width.
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
59
|
+
| Prop | Type | Default | Description |
|
|
60
|
+
| :----------- | :------------------------------------------------- | :---------- | :-------------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| `variant` | `'solid' \| 'outline' \| 'ghost'` | `'solid'` | Visual style variant. |
|
|
62
|
+
| `color` | `SemanticColor` | `'default'` | Semantic color intent (primary, success, etc). |
|
|
63
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | The size of the button. |
|
|
64
|
+
| `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | The corner radius. |
|
|
65
|
+
| `loading` | `boolean` | `false` | Shows a spinner and disables interaction. |
|
|
66
|
+
| `disabled` | `boolean` | `false` | Disables the button. |
|
|
67
|
+
| `fullWidth` | `boolean` | `false` | Expands to fill the container width. |
|
|
68
|
+
| `isIconOnly` | `boolean` | `false` | Formats the button for icon-only display (square, equal padding). Enforces accessibility props at compile-time. |
|
|
69
|
+
| `startIcon` | `ReactNode` | — | Icon displayed before the label. |
|
|
70
|
+
| `endIcon` | `ReactNode` | — | Icon displayed after the label. |
|
|
71
|
+
| `shortcut` | `string` | — | Keyboard shortcut hint. |
|
|
72
|
+
| `classNames` | `ButtonClassNames` | — | Custom classes for internal slots. |
|
|
72
73
|
|
|
73
74
|
### Granular Styling (`classNames`)
|
|
74
75
|
|
|
@@ -93,6 +94,7 @@ The `classNames` prop allows you to override styles for specific internal elemen
|
|
|
93
94
|
- Proper `aria-live` regions for loading states.
|
|
94
95
|
- High-contrast focus rings for keyboard navigation.
|
|
95
96
|
- Semantic HTML tags based on the `as` prop.
|
|
97
|
+
- **Icon-Only Trigger Safety**: When `isIconOnly` is `true`, TypeScript compile-time checks enforce that either `aria-label` or `aria-labelledby` is supplied to ensure the button has a screen-readable name (WCAG 2.1 AA).
|
|
96
98
|
|
|
97
99
|
## License
|
|
98
100
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {Button}from'react-aria-components';import {cn,mergePropsWithContext,getAccessibleName}from'@ideasui/utils';import {createContext,forwardRef,useContext}from'react';import {button}from'@ideasui/theme/recipes';import {jsx,jsxs,Fragment}from'react/jsx-runtime';var
|
|
2
|
-
export{
|
|
1
|
+
import {Button}from'react-aria-components';import {cn,mergePropsWithContext,getAccessibleName}from'@ideasui/utils';import {createContext,forwardRef,useMemo,useContext}from'react';import {button}from'@ideasui/theme/recipes';import {jsx,jsxs,Fragment}from'react/jsx-runtime';var g=createContext(null);function G(){return useContext(g)}var S=forwardRef(({children:o,className:c,size:t,color:r,variant:a,isDisabled:l,isAttached:n=true,isVertical:u=false,radius:d="md",fullWidth:i,disableAnimation:p=false,divider:m="full",isIconOnly:B,...P},e)=>{let C=useMemo(()=>({size:t,color:r,variant:a,isDisabled:l,isAttached:n,isVertical:u,radius:d,fullWidth:i,disableAnimation:p,divider:m,isIconOnly:B}),[t,r,a,l,n,u,d,i,p,m,B]),b="isolate";n||(b=u?"gap-y-2":"gap-x-2");let N=cn(i?"flex":"inline-flex",u?"flex-col":"flex-row",b,i&&"w-full",c);return jsx(g.Provider,{value:C,children:jsx("div",{ref:e,className:N,"data-attached":n,"data-full-width":i,"data-slot":"button-group","data-vertical":u,...P,children:o})})});S.displayName="IdeasUI.Button.Group";var M=createContext(null);function h(){let o=useContext(M);if(!o)throw new Error("Button sub-components must be rendered within a Button component");return o}var f=forwardRef(({children:o,className:c,placement:t="start",...r},a)=>{let{styles:l,classNames:n}=h(),{icon:u}=l,d=t==="start"?n?.startIcon:n?.endIcon;return jsx("span",{ref:a,"aria-hidden":"true",className:cn(u(),t==="end"?"order-last":"order-first",d,c),"data-slot":"button-icon",...r,children:o})});f.displayName="IdeasUI.Button.Icon";var L=forwardRef(({className:o,label:c="Loading",...t},r)=>{let{styles:a,classNames:l}=h(),{loader:n,icon:u}=a;return jsxs("span",{ref:r,className:cn(n(),u(),"inline-flex shrink-0 items-center justify-center",l?.spinner,o),"data-slot":"button-spinner",...t,children:[jsx("svg",{className:"size-full animate-spin",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:"2.5",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:jsx("path",{d:"M21 12a9 9 0 1 1-6.219-8.56"})}),jsx("span",{className:"sr-only",children:c})]})});L.displayName="IdeasUI.Button.Spinner";var w=forwardRef(({children:o,className:c,...t},r)=>{let{styles:a,classNames:l}=h(),{shortcut:n}=a;return jsx("kbd",{ref:r,className:cn(n(),l?.shortcut,c),"data-slot":"button-shortcut",...t,children:o})});w.displayName="IdeasUI.Button.Shortcut";var R=forwardRef(({children:o,className:c,...t},r)=>{let{styles:a,classNames:l}=h(),{label:n}=a;return jsx("span",{ref:r,className:cn(n(),l?.label,c),"data-slot":"button-label",...t,children:o})});R.displayName="IdeasUI.Button.Label";var F=({isLoading:o,loadingIndicator:c,loadingPosition:t="start",isIconOnly:r,startIcon:a,endIcon:l,shortcut:n,children:u,renderProps:d})=>{let i=typeof u=="function"?u(d):u,p=c||jsx(L,{});if(r||o&&t==="center")return o?p:i;let m=o&&t==="start",B=o&&t==="end";return jsxs(Fragment,{children:[m?jsx(f,{placement:"start",children:p}):null,a&&!m?jsx(f,{placement:"start",children:a}):null,typeof i=="string"||typeof i=="number"?jsx(R,{children:i}):i,l&&!B?jsx(f,{placement:"end",children:l}):null,B?jsx(f,{placement:"end",children:p}):null,n?jsx(w,{children:n}):null]})},H=forwardRef((o,c)=>{let{isLoading:t,loadingIndicator:r,loadingPosition:a,isIconOnly:l,startIcon:n,endIcon:u,shortcut:d,children:i,className:p,classNames:m,...B}=o,P=G(),e=mergePropsWithContext(o,P),C=e.isDisabled||t,b=button({variant:e.variant,size:e.size,color:e.color,radius:e.radius,fullWidth:e.fullWidth,isIconOnly:e.isIconOnly,isDisabled:C,isLoading:t,disableAnimation:e.disableAnimation,isAttached:e.isAttached,isVertical:e.isVertical,elevation:e.elevation,divider:e.divider||(e.isAttached?"full":"none")}),N={styles:b,classNames:m},E=getAccessibleName(e,typeof i=="function"?null:i);return jsx(Button,{ref:c,"aria-busy":t,"aria-label":t&&!E?typeof r=="string"?r:"Loading":E,className:v=>b.base({className:cn(typeof p=="function"?p(v):p,m?.base)}),"data-attached":e.isAttached,"data-slot":"button","data-vertical":e.isVertical,isDisabled:C,isPending:t,...B,children:v=>jsx(M.Provider,{value:N,children:jsx(F,{endIcon:u,isIconOnly:e.isIconOnly,isLoading:t,loadingIndicator:r,loadingPosition:a,renderProps:v,shortcut:d,startIcon:n,children:i})})})});H.displayName="IdeasUI.Button";var I=H;I.Icon=f;I.Shortcut=w;I.Spinner=L;I.Label=R;I.Group=S;
|
|
2
|
+
export{I as Button,S as ButtonGroup,g as ButtonGroupContext,G as useButtonGroupContext};
|
package/package.json
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ideasui/button",
|
|
3
|
-
"version": "0.0.2-
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "IdeasUI<
|
|
3
|
+
"version": "0.0.2-canary.1786301183157",
|
|
4
|
+
"description": "Accessible, high-performance Button and ButtonGroup components for React. Built with Tailwind CSS v4, React Aria, and OKLCH color palettes.",
|
|
5
|
+
"author": "IdeasUI<ideas2logic@gmail.com>",
|
|
6
6
|
"homepage": "https://ideasui.com",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"keywords": [
|
|
9
|
-
"ideasui",
|
|
10
9
|
"react",
|
|
11
10
|
"button",
|
|
12
11
|
"button-group",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
12
|
+
"react-aria",
|
|
13
|
+
"tailwindcss",
|
|
14
|
+
"tailwind-v4",
|
|
15
|
+
"accessible-ui",
|
|
16
|
+
"wcag",
|
|
17
|
+
"ideasui",
|
|
18
|
+
"components",
|
|
16
19
|
"design-system",
|
|
17
|
-
"
|
|
20
|
+
"headless-ui"
|
|
18
21
|
],
|
|
19
22
|
"main": "dist/index.mjs",
|
|
23
|
+
"types": "dist/index.d.mts",
|
|
20
24
|
"exports": {
|
|
21
25
|
".": {
|
|
22
26
|
"types": "./dist/index.d.mts",
|
|
@@ -46,9 +50,8 @@
|
|
|
46
50
|
},
|
|
47
51
|
"dependencies": {
|
|
48
52
|
"react-aria-components": "^1.16.0",
|
|
49
|
-
"@ideasui/theme": "0.0.5-
|
|
50
|
-
"@ideasui/utils": "0.0.2-
|
|
53
|
+
"@ideasui/theme": "0.0.5-canary.1786301183157",
|
|
54
|
+
"@ideasui/utils": "0.0.2-canary.1786301183157"
|
|
51
55
|
},
|
|
52
|
-
"module": "dist/index.mjs"
|
|
53
|
-
"types": "dist/index.d.mts"
|
|
56
|
+
"module": "dist/index.mjs"
|
|
54
57
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type { ButtonProps } from './button.types';
|
|
3
|
+
import type { ButtonProps as ButtonProperties } from './button.types';
|
|
4
4
|
|
|
5
5
|
import { createContext, useContext } from 'react';
|
|
6
6
|
|
|
7
7
|
export interface ButtonGroupContextType {
|
|
8
|
-
size?:
|
|
9
|
-
color?:
|
|
10
|
-
variant?:
|
|
8
|
+
size?: ButtonProperties['size'];
|
|
9
|
+
color?: ButtonProperties['color'];
|
|
10
|
+
variant?: ButtonProperties['variant'];
|
|
11
11
|
isDisabled?: boolean;
|
|
12
12
|
isAttached?: boolean;
|
|
13
13
|
isVertical?: boolean;
|
|
14
|
-
radius?:
|
|
14
|
+
radius?: ButtonProperties['radius'];
|
|
15
15
|
fullWidth?: boolean;
|
|
16
16
|
disableAnimation?: boolean;
|
|
17
|
-
|
|
17
|
+
divider?: ButtonProperties['divider'];
|
|
18
|
+
isIconOnly?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const ButtonGroupContext = createContext<ButtonGroupContextType | null>(null);
|
package/src/button-group.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { HTMLAttributes, ReactNode, JSX } from 'react';
|
|
4
4
|
import type { ButtonGroupContextType } from './button-group-context';
|
|
5
5
|
|
|
6
|
-
import { forwardRef } from 'react';
|
|
6
|
+
import { forwardRef, useMemo } from 'react';
|
|
7
7
|
import { cn } from '@ideasui/utils';
|
|
8
8
|
|
|
9
9
|
import { ButtonGroupContext } from './button-group-context';
|
|
@@ -12,7 +12,7 @@ import { ButtonGroupContext } from './button-group-context';
|
|
|
12
12
|
* ButtonGroup Component
|
|
13
13
|
* ---------------------------------------------------------------------------------------------*/
|
|
14
14
|
|
|
15
|
-
export interface
|
|
15
|
+
export interface ButtonGroupProperties
|
|
16
16
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'color'>, ButtonGroupContextType {
|
|
17
17
|
/**
|
|
18
18
|
* Whether the buttons in the group should be vertical.
|
|
@@ -25,17 +25,17 @@ export interface ButtonGroupProps
|
|
|
25
25
|
*/
|
|
26
26
|
isAttached?: boolean;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
* @default
|
|
28
|
+
* The type of divider to display between the buttons in the group.
|
|
29
|
+
* @default 'full' (when isAttached is true)
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
divider?: 'full' | 'middle' | 'none';
|
|
32
32
|
/**
|
|
33
33
|
* The content of the button group.
|
|
34
34
|
*/
|
|
35
35
|
children: ReactNode;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export const ButtonGroup = forwardRef<HTMLDivElement,
|
|
38
|
+
export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProperties>(
|
|
39
39
|
(
|
|
40
40
|
{
|
|
41
41
|
children,
|
|
@@ -46,31 +46,54 @@ export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
|
46
46
|
isDisabled,
|
|
47
47
|
isAttached = true,
|
|
48
48
|
isVertical = false,
|
|
49
|
-
radius,
|
|
49
|
+
radius = 'md',
|
|
50
50
|
fullWidth,
|
|
51
|
-
disableAnimation =
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
disableAnimation = false,
|
|
52
|
+
divider = 'full',
|
|
53
|
+
isIconOnly,
|
|
54
|
+
...properties
|
|
54
55
|
},
|
|
55
|
-
|
|
56
|
+
reference,
|
|
56
57
|
): JSX.Element => {
|
|
57
|
-
const contextValue =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
const contextValue = useMemo(
|
|
59
|
+
() => ({
|
|
60
|
+
size,
|
|
61
|
+
color,
|
|
62
|
+
variant,
|
|
63
|
+
isDisabled,
|
|
64
|
+
isAttached,
|
|
65
|
+
isVertical,
|
|
66
|
+
radius,
|
|
67
|
+
fullWidth,
|
|
68
|
+
disableAnimation,
|
|
69
|
+
divider,
|
|
70
|
+
isIconOnly,
|
|
71
|
+
}),
|
|
72
|
+
[
|
|
73
|
+
size,
|
|
74
|
+
color,
|
|
75
|
+
variant,
|
|
76
|
+
isDisabled,
|
|
77
|
+
isAttached,
|
|
78
|
+
isVertical,
|
|
79
|
+
radius,
|
|
80
|
+
fullWidth,
|
|
81
|
+
disableAnimation,
|
|
82
|
+
divider,
|
|
83
|
+
isIconOnly,
|
|
84
|
+
],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
let gapClass = 'isolate';
|
|
88
|
+
|
|
89
|
+
if (!isAttached) {
|
|
90
|
+
gapClass = isVertical ? 'gap-y-2' : 'gap-x-2';
|
|
91
|
+
}
|
|
69
92
|
|
|
70
93
|
const groupClasses = cn(
|
|
71
94
|
fullWidth ? 'flex' : 'inline-flex',
|
|
72
95
|
isVertical ? 'flex-col' : 'flex-row',
|
|
73
|
-
|
|
96
|
+
gapClass,
|
|
74
97
|
fullWidth && 'w-full',
|
|
75
98
|
className,
|
|
76
99
|
);
|
|
@@ -78,14 +101,13 @@ export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
|
78
101
|
return (
|
|
79
102
|
<ButtonGroupContext.Provider value={contextValue}>
|
|
80
103
|
<div
|
|
81
|
-
ref={
|
|
104
|
+
ref={reference}
|
|
82
105
|
className={groupClasses}
|
|
83
106
|
data-attached={isAttached}
|
|
84
107
|
data-full-width={fullWidth}
|
|
85
108
|
data-slot="button-group"
|
|
86
109
|
data-vertical={isVertical}
|
|
87
|
-
|
|
88
|
-
{...props}
|
|
110
|
+
{...properties}
|
|
89
111
|
>
|
|
90
112
|
{children}
|
|
91
113
|
</div>
|
package/src/button.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
ButtonProps,
|
|
4
|
+
ButtonProps as ButtonProperties,
|
|
5
5
|
ButtonComponent,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
ButtonClassNames,
|
|
7
|
+
ButtonLabelProperties,
|
|
8
|
+
ButtonIconProperties,
|
|
9
|
+
ButtonSpinnerProperties,
|
|
10
|
+
ButtonShortcutProperties,
|
|
10
11
|
} from './button.types';
|
|
11
12
|
import type { ButtonReturnType } from '@ideasui/theme/recipes';
|
|
12
13
|
import type { ButtonRenderProps } from 'react-aria-components';
|
|
@@ -29,7 +30,7 @@ import { ButtonGroup } from './button-group';
|
|
|
29
30
|
*/
|
|
30
31
|
interface ButtonContextValue {
|
|
31
32
|
styles: ButtonReturnType;
|
|
32
|
-
classNames?:
|
|
33
|
+
classNames?: ButtonClassNames;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const ButtonContext = createContext<ButtonContextValue | null>(null);
|
|
@@ -53,8 +54,8 @@ function useButtonContext(): ButtonContextValue {
|
|
|
53
54
|
* Button Slots
|
|
54
55
|
* ---------------------------------------------------------------------------------------------*/
|
|
55
56
|
|
|
56
|
-
const ButtonIcon = forwardRef<HTMLElement,
|
|
57
|
-
({ children, className, placement = 'start', ...
|
|
57
|
+
const ButtonIcon = forwardRef<HTMLElement, ButtonIconProperties>(
|
|
58
|
+
({ children, className, placement = 'start', ...properties }, reference): JSX.Element => {
|
|
58
59
|
const { styles, classNames } = useButtonContext();
|
|
59
60
|
const { icon } = styles;
|
|
60
61
|
|
|
@@ -62,7 +63,7 @@ const ButtonIcon = forwardRef<HTMLElement, ButtonIconProps>(
|
|
|
62
63
|
|
|
63
64
|
return (
|
|
64
65
|
<span
|
|
65
|
-
ref={
|
|
66
|
+
ref={reference}
|
|
66
67
|
aria-hidden="true"
|
|
67
68
|
className={cn(
|
|
68
69
|
icon(),
|
|
@@ -71,7 +72,7 @@ const ButtonIcon = forwardRef<HTMLElement, ButtonIconProps>(
|
|
|
71
72
|
className,
|
|
72
73
|
)}
|
|
73
74
|
data-slot="button-icon"
|
|
74
|
-
{...
|
|
75
|
+
{...properties}
|
|
75
76
|
>
|
|
76
77
|
{children}
|
|
77
78
|
</span>
|
|
@@ -81,14 +82,14 @@ const ButtonIcon = forwardRef<HTMLElement, ButtonIconProps>(
|
|
|
81
82
|
|
|
82
83
|
ButtonIcon.displayName = 'IdeasUI.Button.Icon';
|
|
83
84
|
|
|
84
|
-
const ButtonSpinner = forwardRef<HTMLSpanElement,
|
|
85
|
-
({ className, label = 'Loading', ...
|
|
85
|
+
const ButtonSpinner = forwardRef<HTMLSpanElement, ButtonSpinnerProperties>(
|
|
86
|
+
({ className, label = 'Loading', ...properties }, reference): JSX.Element => {
|
|
86
87
|
const { styles, classNames } = useButtonContext();
|
|
87
88
|
const { loader, icon } = styles;
|
|
88
89
|
|
|
89
90
|
return (
|
|
90
91
|
<span
|
|
91
|
-
ref={
|
|
92
|
+
ref={reference}
|
|
92
93
|
className={cn(
|
|
93
94
|
loader(),
|
|
94
95
|
icon(),
|
|
@@ -97,8 +98,7 @@ const ButtonSpinner = forwardRef<HTMLSpanElement, ButtonSpinnerProps>(
|
|
|
97
98
|
className,
|
|
98
99
|
)}
|
|
99
100
|
data-slot="button-spinner"
|
|
100
|
-
|
|
101
|
-
{...props}
|
|
101
|
+
{...properties}
|
|
102
102
|
>
|
|
103
103
|
<svg
|
|
104
104
|
className="size-full animate-spin"
|
|
@@ -120,17 +120,17 @@ const ButtonSpinner = forwardRef<HTMLSpanElement, ButtonSpinnerProps>(
|
|
|
120
120
|
|
|
121
121
|
ButtonSpinner.displayName = 'IdeasUI.Button.Spinner';
|
|
122
122
|
|
|
123
|
-
const ButtonShortcut = forwardRef<HTMLSpanElement,
|
|
124
|
-
({ children, className, ...
|
|
123
|
+
const ButtonShortcut = forwardRef<HTMLSpanElement, ButtonShortcutProperties>(
|
|
124
|
+
({ children, className, ...properties }, reference): JSX.Element => {
|
|
125
125
|
const { styles, classNames } = useButtonContext();
|
|
126
126
|
const { shortcut } = styles;
|
|
127
127
|
|
|
128
128
|
return (
|
|
129
129
|
<kbd
|
|
130
|
-
ref={
|
|
130
|
+
ref={reference}
|
|
131
131
|
className={cn(shortcut(), classNames?.shortcut, className)}
|
|
132
132
|
data-slot="button-shortcut"
|
|
133
|
-
{...
|
|
133
|
+
{...properties}
|
|
134
134
|
>
|
|
135
135
|
{children}
|
|
136
136
|
</kbd>
|
|
@@ -140,17 +140,17 @@ const ButtonShortcut = forwardRef<HTMLSpanElement, ButtonShortcutProps>(
|
|
|
140
140
|
|
|
141
141
|
ButtonShortcut.displayName = 'IdeasUI.Button.Shortcut';
|
|
142
142
|
|
|
143
|
-
const ButtonLabel = forwardRef<HTMLSpanElement,
|
|
144
|
-
({ children, className, ...
|
|
143
|
+
const ButtonLabel = forwardRef<HTMLSpanElement, ButtonLabelProperties>(
|
|
144
|
+
({ children, className, ...properties }, reference): JSX.Element => {
|
|
145
145
|
const { styles, classNames } = useButtonContext();
|
|
146
146
|
const { label } = styles;
|
|
147
147
|
|
|
148
148
|
return (
|
|
149
149
|
<span
|
|
150
|
-
ref={
|
|
150
|
+
ref={reference}
|
|
151
151
|
className={cn(label(), classNames?.label, className)}
|
|
152
152
|
data-slot="button-label"
|
|
153
|
-
{...
|
|
153
|
+
{...properties}
|
|
154
154
|
>
|
|
155
155
|
{children}
|
|
156
156
|
</span>
|
|
@@ -164,7 +164,7 @@ ButtonLabel.displayName = 'IdeasUI.Button.Label';
|
|
|
164
164
|
* Button Render Helpers
|
|
165
165
|
* ---------------------------------------------------------------------------------------------*/
|
|
166
166
|
|
|
167
|
-
interface
|
|
167
|
+
interface ButtonContentProperties {
|
|
168
168
|
isLoading?: boolean;
|
|
169
169
|
loadingIndicator?: React.ReactNode;
|
|
170
170
|
loadingPosition?: 'start' | 'end' | 'center';
|
|
@@ -172,7 +172,7 @@ interface ButtonContentProps {
|
|
|
172
172
|
startIcon?: React.ReactNode;
|
|
173
173
|
endIcon?: React.ReactNode;
|
|
174
174
|
shortcut?: React.ReactNode;
|
|
175
|
-
children:
|
|
175
|
+
children: ButtonProperties['children'];
|
|
176
176
|
renderProps: ButtonRenderProps;
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -186,7 +186,8 @@ const ButtonContent = ({
|
|
|
186
186
|
shortcut,
|
|
187
187
|
children,
|
|
188
188
|
renderProps,
|
|
189
|
-
|
|
189
|
+
// eslint-disable-next-line sonarjs/function-return-type
|
|
190
|
+
}: ButtonContentProperties): ReactNode => {
|
|
190
191
|
const content = typeof children === 'function' ? children(renderProps) : children;
|
|
191
192
|
const loader = loadingIndicator || <ButtonSpinner />;
|
|
192
193
|
|
|
@@ -224,86 +225,92 @@ const ButtonContent = ({
|
|
|
224
225
|
* Button Base
|
|
225
226
|
* ---------------------------------------------------------------------------------------------*/
|
|
226
227
|
|
|
227
|
-
const ButtonBase = forwardRef<HTMLButtonElement,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
228
|
+
const ButtonBase = forwardRef<HTMLButtonElement, ButtonProperties>(
|
|
229
|
+
(originalProperties, reference): JSX.Element => {
|
|
230
|
+
const {
|
|
231
|
+
isLoading,
|
|
232
|
+
loadingIndicator,
|
|
233
|
+
loadingPosition,
|
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
235
|
+
isIconOnly,
|
|
236
|
+
startIcon,
|
|
237
|
+
endIcon,
|
|
238
|
+
shortcut,
|
|
239
|
+
children,
|
|
240
|
+
className,
|
|
241
|
+
classNames,
|
|
242
|
+
...properties
|
|
243
|
+
} = originalProperties;
|
|
244
|
+
|
|
245
|
+
const groupContext = useButtonGroupContext();
|
|
246
|
+
const merged = mergePropsWithContext(originalProperties, groupContext);
|
|
247
|
+
|
|
248
|
+
// Specific logic for Button
|
|
249
|
+
const mergedDisabled = merged.isDisabled || isLoading;
|
|
250
|
+
|
|
251
|
+
const styles = button({
|
|
252
|
+
variant: merged.variant,
|
|
253
|
+
size: merged.size,
|
|
254
|
+
color: merged.color,
|
|
255
|
+
radius: merged.radius,
|
|
256
|
+
fullWidth: merged.fullWidth,
|
|
257
|
+
isIconOnly: merged.isIconOnly,
|
|
258
|
+
isDisabled: mergedDisabled,
|
|
259
|
+
isLoading,
|
|
260
|
+
disableAnimation: merged.disableAnimation,
|
|
261
|
+
isAttached: merged.isAttached,
|
|
262
|
+
isVertical: merged.isVertical,
|
|
263
|
+
elevation: merged.elevation,
|
|
264
|
+
divider: merged.divider || (merged.isAttached ? 'full' : 'none'),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Context value - React Compiler will optimize this automatically
|
|
268
|
+
const contextValue = { styles, classNames };
|
|
269
|
+
|
|
270
|
+
// Ensure we have an accessible name when loading or icon-only
|
|
271
|
+
const ariaLabel = getAccessibleName(merged, typeof children === 'function' ? null : children);
|
|
272
|
+
const loadingLabel = typeof loadingIndicator === 'string' ? loadingIndicator : 'Loading';
|
|
267
273
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
274
|
+
return (
|
|
275
|
+
<ButtonPrimitive
|
|
276
|
+
ref={reference}
|
|
277
|
+
aria-busy={isLoading}
|
|
278
|
+
aria-label={isLoading && !ariaLabel ? loadingLabel : ariaLabel}
|
|
279
|
+
className={(renderProperties) =>
|
|
280
|
+
styles.base({
|
|
281
|
+
className: cn(
|
|
282
|
+
typeof className === 'function' ? className(renderProperties) : className,
|
|
283
|
+
classNames?.base,
|
|
284
|
+
),
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
data-attached={merged.isAttached}
|
|
288
|
+
data-slot="button"
|
|
289
|
+
data-vertical={merged.isVertical}
|
|
290
|
+
isDisabled={mergedDisabled}
|
|
291
|
+
isPending={isLoading}
|
|
292
|
+
{...properties}
|
|
293
|
+
>
|
|
294
|
+
{(renderProperties) => (
|
|
295
|
+
<ButtonContext.Provider value={contextValue}>
|
|
296
|
+
<ButtonContent
|
|
297
|
+
endIcon={endIcon}
|
|
298
|
+
isIconOnly={merged.isIconOnly}
|
|
299
|
+
isLoading={isLoading}
|
|
300
|
+
loadingIndicator={loadingIndicator}
|
|
301
|
+
loadingPosition={loadingPosition}
|
|
302
|
+
renderProps={renderProperties}
|
|
303
|
+
shortcut={shortcut}
|
|
304
|
+
startIcon={startIcon}
|
|
305
|
+
>
|
|
306
|
+
{children}
|
|
307
|
+
</ButtonContent>
|
|
308
|
+
</ButtonContext.Provider>
|
|
309
|
+
)}
|
|
310
|
+
</ButtonPrimitive>
|
|
311
|
+
);
|
|
312
|
+
},
|
|
313
|
+
);
|
|
307
314
|
|
|
308
315
|
ButtonBase.displayName = 'IdeasUI.Button';
|
|
309
316
|
|
package/src/button.types.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ButtonProps as ButtonPrimitiveProperties,
|
|
3
|
+
ButtonRenderProps,
|
|
4
|
+
} from 'react-aria-components';
|
|
2
5
|
import type { ForwardRefExoticComponent, HTMLAttributes, ReactNode, RefAttributes } from 'react';
|
|
3
6
|
import type { ButtonVariantProps } from '@ideasui/theme/recipes';
|
|
4
|
-
import type {
|
|
7
|
+
import type { ButtonGroupProperties } from './button-group';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Granular class names for the Button component slots.
|
|
@@ -22,11 +25,11 @@ export interface ButtonClassNames {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
|
-
*
|
|
28
|
+
* Base props for the Button component.
|
|
26
29
|
*/
|
|
27
|
-
|
|
30
|
+
interface ButtonBaseProperties
|
|
28
31
|
extends
|
|
29
|
-
Omit<
|
|
32
|
+
Omit<ButtonPrimitiveProperties, 'className'>,
|
|
30
33
|
Omit<ButtonVariantProps, 'isLoading' | 'isDisabled'> {
|
|
31
34
|
/**
|
|
32
35
|
* Whether the button is in a loading state.
|
|
@@ -43,13 +46,6 @@ export interface ButtonProps
|
|
|
43
46
|
* @default 'start'
|
|
44
47
|
*/
|
|
45
48
|
loadingPosition?: 'start' | 'end' | 'center';
|
|
46
|
-
/**
|
|
47
|
-
* Whether the button should be square and optimized for icons.
|
|
48
|
-
*
|
|
49
|
-
* Note: When true, you MUST provide an `aria-label` or `aria-labelledby` for accessibility.
|
|
50
|
-
* @default false
|
|
51
|
-
*/
|
|
52
|
-
isIconOnly?: boolean;
|
|
53
49
|
/**
|
|
54
50
|
* Icon to display before the button content.
|
|
55
51
|
*/
|
|
@@ -63,10 +59,10 @@ export interface ButtonProps
|
|
|
63
59
|
*/
|
|
64
60
|
shortcut?: ReactNode;
|
|
65
61
|
/**
|
|
66
|
-
*
|
|
67
|
-
* @default
|
|
62
|
+
* The type of divider to display between the buttons in a group.
|
|
63
|
+
* @default 'full' (when isAttached is true)
|
|
68
64
|
*/
|
|
69
|
-
|
|
65
|
+
divider?: 'full' | 'middle' | 'none';
|
|
70
66
|
/**
|
|
71
67
|
* Custom class names for individual button slots.
|
|
72
68
|
*/
|
|
@@ -75,12 +71,19 @@ export interface ButtonProps
|
|
|
75
71
|
* The CSS class name for the button.
|
|
76
72
|
* Can be a string or a function that receives the button render props.
|
|
77
73
|
*/
|
|
78
|
-
className?: string | ((
|
|
74
|
+
className?: string | ((properties: ButtonRenderProps) => string);
|
|
79
75
|
/**
|
|
80
76
|
* The content to display inside the button.
|
|
81
77
|
* Can be a ReactNode or a function that receives the button render props.
|
|
82
78
|
*/
|
|
83
|
-
children?: ReactNode | ((
|
|
79
|
+
children?: ReactNode | ((properties: ButtonRenderProps) => ReactNode);
|
|
80
|
+
/**
|
|
81
|
+
* Whether the button should be square and optimized for icons.
|
|
82
|
+
*
|
|
83
|
+
* Note: When true, you MUST provide an `aria-label` or `aria-labelledby` for accessibility.
|
|
84
|
+
* @default false
|
|
85
|
+
*/
|
|
86
|
+
isIconOnly?: boolean;
|
|
84
87
|
/**
|
|
85
88
|
* Accessibility label for the button.
|
|
86
89
|
* Required if the button has no visible label (e.g., `isIconOnly`).
|
|
@@ -92,15 +95,67 @@ export interface ButtonProps
|
|
|
92
95
|
'aria-labelledby'?: string;
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Props for the Button component.
|
|
100
|
+
*
|
|
101
|
+
* Enforces that if `isIconOnly` is true, either `aria-label` or `aria-labelledby` must be provided.
|
|
102
|
+
*/
|
|
103
|
+
export type ButtonProps =
|
|
104
|
+
| (ButtonBaseProperties & {
|
|
105
|
+
/**
|
|
106
|
+
* Whether the button should be square and optimized for icons.
|
|
107
|
+
* @default false
|
|
108
|
+
*/
|
|
109
|
+
isIconOnly?: false;
|
|
110
|
+
/**
|
|
111
|
+
* Accessibility label for the button.
|
|
112
|
+
*/
|
|
113
|
+
'aria-label'?: string;
|
|
114
|
+
/**
|
|
115
|
+
* ID of an element that serves as the accessible label for the button.
|
|
116
|
+
*/
|
|
117
|
+
'aria-labelledby'?: string;
|
|
118
|
+
})
|
|
119
|
+
| (ButtonBaseProperties & {
|
|
120
|
+
/**
|
|
121
|
+
* Whether the button should be square and optimized for icons.
|
|
122
|
+
*/
|
|
123
|
+
isIconOnly: true;
|
|
124
|
+
/**
|
|
125
|
+
* Accessibility label for the button.
|
|
126
|
+
* Required if the button has no visible label (e.g., `isIconOnly`).
|
|
127
|
+
*/
|
|
128
|
+
'aria-label': string;
|
|
129
|
+
/**
|
|
130
|
+
* ID of an element that serves as the accessible label for the button.
|
|
131
|
+
*/
|
|
132
|
+
'aria-labelledby'?: string;
|
|
133
|
+
})
|
|
134
|
+
| (ButtonBaseProperties & {
|
|
135
|
+
/**
|
|
136
|
+
* Whether the button should be square and optimized for icons.
|
|
137
|
+
*/
|
|
138
|
+
isIconOnly: true;
|
|
139
|
+
/**
|
|
140
|
+
* Accessibility label for the button.
|
|
141
|
+
*/
|
|
142
|
+
'aria-label'?: string;
|
|
143
|
+
/**
|
|
144
|
+
* ID of an element that serves as the accessible label for the button.
|
|
145
|
+
* Required if the button has no visible label (e.g., `isIconOnly`).
|
|
146
|
+
*/
|
|
147
|
+
'aria-labelledby': string;
|
|
148
|
+
});
|
|
149
|
+
|
|
95
150
|
/**
|
|
96
151
|
* Props for the Button.Label component.
|
|
97
152
|
*/
|
|
98
|
-
export interface
|
|
153
|
+
export interface ButtonLabelProperties extends HTMLAttributes<HTMLSpanElement> {}
|
|
99
154
|
|
|
100
155
|
/**
|
|
101
156
|
* Props for the Button.Icon component.
|
|
102
157
|
*/
|
|
103
|
-
export interface
|
|
158
|
+
export interface ButtonIconProperties extends HTMLAttributes<HTMLElement> {
|
|
104
159
|
/**
|
|
105
160
|
* The icon content.
|
|
106
161
|
*/
|
|
@@ -115,7 +170,7 @@ export interface ButtonIconProps extends HTMLAttributes<HTMLElement> {
|
|
|
115
170
|
/**
|
|
116
171
|
* Props for the Button.Spinner component.
|
|
117
172
|
*/
|
|
118
|
-
export interface
|
|
173
|
+
export interface ButtonSpinnerProperties extends HTMLAttributes<HTMLSpanElement> {
|
|
119
174
|
/**
|
|
120
175
|
* Accessibility label for the spinner.
|
|
121
176
|
* @default 'Loading'
|
|
@@ -126,7 +181,7 @@ export interface ButtonSpinnerProps extends HTMLAttributes<HTMLSpanElement> {
|
|
|
126
181
|
/**
|
|
127
182
|
* Props for the Button.Shortcut component.
|
|
128
183
|
*/
|
|
129
|
-
export interface
|
|
184
|
+
export interface ButtonShortcutProperties extends HTMLAttributes<HTMLSpanElement> {
|
|
130
185
|
/**
|
|
131
186
|
* The shortcut keys to display.
|
|
132
187
|
*/
|
|
@@ -142,21 +197,21 @@ export interface ButtonComponent extends ForwardRefExoticComponent<
|
|
|
142
197
|
/**
|
|
143
198
|
* Component to display the button label.
|
|
144
199
|
*/
|
|
145
|
-
Label: ForwardRefExoticComponent<
|
|
200
|
+
Label: ForwardRefExoticComponent<ButtonLabelProperties & RefAttributes<HTMLSpanElement>>;
|
|
146
201
|
/**
|
|
147
202
|
* Component to display an icon inside the button.
|
|
148
203
|
*/
|
|
149
|
-
Icon: ForwardRefExoticComponent<
|
|
204
|
+
Icon: ForwardRefExoticComponent<ButtonIconProperties & RefAttributes<HTMLElement>>;
|
|
150
205
|
/**
|
|
151
206
|
* Component to display a shortcut indicator inside the button.
|
|
152
207
|
*/
|
|
153
|
-
Shortcut: ForwardRefExoticComponent<
|
|
208
|
+
Shortcut: ForwardRefExoticComponent<ButtonShortcutProperties & RefAttributes<HTMLSpanElement>>;
|
|
154
209
|
/**
|
|
155
210
|
* Component to display a loading spinner inside the button.
|
|
156
211
|
*/
|
|
157
|
-
Spinner: ForwardRefExoticComponent<
|
|
212
|
+
Spinner: ForwardRefExoticComponent<ButtonSpinnerProperties & RefAttributes<HTMLSpanElement>>;
|
|
158
213
|
/**
|
|
159
214
|
* Component to group multiple buttons together.
|
|
160
215
|
*/
|
|
161
|
-
Group: ForwardRefExoticComponent<
|
|
216
|
+
Group: ForwardRefExoticComponent<ButtonGroupProperties & RefAttributes<HTMLDivElement>>;
|
|
162
217
|
}
|