@maratus-registry/button 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ import type { ButtonProps } from './useButton'
2
+ import { useButton } from './useButton'
3
+
4
+ export function Button(props: ButtonProps) {
5
+ const {
6
+ disabled,
7
+ disabledBehavior = 'native',
8
+ isLoading = false,
9
+ onClick,
10
+ onKeyDown,
11
+ onKeyUp,
12
+ onMouseDown,
13
+ onPointerDown,
14
+ onTouchStart,
15
+ type,
16
+ ...hookProps
17
+ } = props
18
+ const { buttonProps, preventDisabledActivation, whenEnabled } = useButton({
19
+ ...hookProps,
20
+ disabled,
21
+ disabledBehavior,
22
+ isLoading,
23
+ })
24
+ const { children, ...rootProps } = buttonProps
25
+ const isInteractionDisabled = disabled || isLoading
26
+
27
+ return (
28
+ <button
29
+ {...rootProps}
30
+ disabled={isInteractionDisabled && disabledBehavior === 'native'}
31
+ {...preventDisabledActivation({
32
+ onKeyDown,
33
+ onKeyUp,
34
+ })}
35
+ {...whenEnabled({
36
+ onClick,
37
+ onMouseDown,
38
+ onPointerDown,
39
+ onTouchStart,
40
+ })}
41
+ type={type}
42
+ >
43
+ {children}
44
+ </button>
45
+ )
46
+ }
@@ -0,0 +1,87 @@
1
+ @layer components {
2
+ :root {
3
+ --ara-button-bg: var(--ara-color-control-bg);
4
+ --ara-button-bg--disabled: var(--ara-color-control-bg--disabled);
5
+ --ara-button-bg--focus: var(--ara-color-control-bg--focus);
6
+ --ara-button-bg--hover: var(--ara-color-control-bg--hover);
7
+ --ara-button-bg--loading: var(--ara-color-control-bg--loading);
8
+ --ara-button-bg--pressed: var(--ara-color-control-bg--pressed);
9
+ --ara-button-detail: var(--ara-color-control-detail);
10
+ --ara-button-detail--disabled: var(--ara-color-control-detail--disabled);
11
+ --ara-button-detail--focus: var(--ara-color-control-detail--focus);
12
+ --ara-button-detail--hover: var(--ara-color-control-detail--hover);
13
+ --ara-button-detail--loading: var(--ara-color-control-detail--loading);
14
+ --ara-button-detail--pressed: var(--ara-color-control-detail--pressed);
15
+ --ara-button-border-radius: var(--ara-radius-x1);
16
+ --ara-button-border-width: var(--ara-border-width-x1);
17
+ --ara-button-focus: var(--ara-color-control-focus);
18
+ --ara-button-focus-offset: var(--ara-spacing-x1);
19
+ --ara-button-focus-width: var(--ara-border-width-x1);
20
+ --ara-button-padding-block: var(--ara-spacing-x1);
21
+ --ara-button-padding-inline: var(--ara-spacing-x1);
22
+ --ara-button-fg: var(--ara-color-control-fg);
23
+ --ara-button-fg--disabled: var(--ara-color-control-fg--disabled);
24
+ --ara-button-fg--focus: var(--ara-color-control-fg--focus);
25
+ --ara-button-fg--hover: var(--ara-color-control-fg--hover);
26
+ --ara-button-fg--loading: var(--ara-color-control-fg--loading);
27
+ --ara-button-fg--pressed: var(--ara-color-control-fg--pressed);
28
+ --ara-button-shadow: var(--ara-shadow-control);
29
+ --ara-button-shadow--disabled: var(--ara-shadow-control--disabled);
30
+ --ara-button-shadow--focus: var(--ara-shadow-control--focus);
31
+ --ara-button-shadow--hover: var(--ara-shadow-control--hover);
32
+ --ara-button-shadow--loading: var(--ara-shadow-control--loading);
33
+ --ara-button-shadow--pressed: var(--ara-shadow-control--pressed);
34
+ }
35
+
36
+ .maratus__button__button {
37
+ background-color: var(--ara-button-bg);
38
+ border-color: var(--ara-button-detail);
39
+ border-radius: var(--ara-button-border-radius);
40
+ border-width: var(--ara-button-border-width);
41
+ box-shadow: var(--ara-button-shadow);
42
+ color: var(--ara-button-fg);
43
+ padding-block: var(--ara-button-padding-block);
44
+ padding-inline: var(--ara-button-padding-inline);
45
+ }
46
+
47
+ .maratus__button__button:hover {
48
+ background-color: var(--ara-button-bg--hover);
49
+ border-color: var(--ara-button-detail--hover);
50
+ box-shadow: var(--ara-button-shadow--hover);
51
+ color: var(--ara-button-fg--hover);
52
+ }
53
+
54
+ .maratus__button__button:focus-visible,
55
+ .maratus__button__button[data-focus-visible] {
56
+ background-color: var(--ara-button-bg--focus);
57
+ border-color: var(--ara-button-detail--focus);
58
+ box-shadow: var(--ara-button-shadow--focus);
59
+ color: var(--ara-button-fg--focus);
60
+ outline-color: var(--ara-button-focus);
61
+ outline-offset: var(--ara-button-focus-offset);
62
+ outline-width: var(--ara-button-focus-width);
63
+ }
64
+
65
+ .maratus__button__button[aria-pressed='true'],
66
+ .maratus__button__button[aria-pressed='mixed'] {
67
+ background-color: var(--ara-button-bg--pressed);
68
+ border-color: var(--ara-button-detail--pressed);
69
+ box-shadow: var(--ara-button-shadow--pressed);
70
+ color: var(--ara-button-fg--pressed);
71
+ }
72
+
73
+ .maratus__button__button:disabled,
74
+ .maratus__button__button[aria-disabled='true'] {
75
+ background-color: var(--ara-button-bg--disabled);
76
+ border-color: var(--ara-button-detail--disabled);
77
+ box-shadow: var(--ara-button-shadow--disabled);
78
+ color: var(--ara-button-fg--disabled);
79
+ }
80
+
81
+ .maratus__button__button[data-loading] {
82
+ background-color: var(--ara-button-bg--loading);
83
+ border-color: var(--ara-button-detail--loading);
84
+ box-shadow: var(--ara-button-shadow--loading);
85
+ color: var(--ara-button-fg--loading);
86
+ }
87
+ }
@@ -0,0 +1,3 @@
1
+ export type { ButtonProps, UseButtonResult } from './useButton'
2
+ export { Button } from './Button'
3
+ export { useButton } from './useButton'
@@ -0,0 +1,143 @@
1
+ import type { ButtonHTMLAttributes, KeyboardEventHandler } from 'react'
2
+ import { useIsFocusVisible } from '@maratus-lib/focus-modality'
3
+ import clsx from 'clsx'
4
+ import { useCallback } from 'react'
5
+ import './button.css'
6
+
7
+ type NativeButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
8
+
9
+ type CommonButtonProps = Omit<NativeButtonProps, 'aria-pressed' | 'role'> & {
10
+ disabledBehavior?: 'native' | 'focusable'
11
+ isLoading?: boolean
12
+ }
13
+
14
+ type CommandButtonProps = {
15
+ kind?: 'command'
16
+ pressed?: never
17
+ }
18
+
19
+ type ToggleButtonProps = {
20
+ kind: 'toggle'
21
+ pressed: boolean | 'mixed'
22
+ }
23
+
24
+ export type ButtonProps = CommonButtonProps &
25
+ (CommandButtonProps | ToggleButtonProps)
26
+
27
+ type ButtonRootProps = NativeButtonProps & {
28
+ 'data-focus-visible'?: ''
29
+ 'data-loading'?: ''
30
+ }
31
+
32
+ type WhenEnabled = <T extends {}>(props: T) => T | {}
33
+
34
+ type ActivationHandlerProps = {
35
+ onKeyDown?: KeyboardEventHandler<HTMLButtonElement>
36
+ onKeyUp?: KeyboardEventHandler<HTMLButtonElement>
37
+ }
38
+
39
+ type PreventDisabledActivation = (
40
+ props: ActivationHandlerProps,
41
+ ) => ActivationHandlerProps
42
+
43
+ export type UseButtonResult = {
44
+ buttonProps: ButtonRootProps
45
+
46
+ /**
47
+ * Helper function that suppresses props when the button is disabled
48
+ */
49
+ whenEnabled: WhenEnabled
50
+
51
+ /**
52
+ * Helper function that prevents keyboard-triggered activation while the
53
+ * button remains focusable.
54
+ */
55
+ preventDisabledActivation: PreventDisabledActivation
56
+ }
57
+
58
+ export function useButton(props: ButtonProps): UseButtonResult {
59
+ const {
60
+ 'aria-busy': ariaBusy,
61
+ 'aria-disabled': ariaDisabled,
62
+ className,
63
+ disabled,
64
+ disabledBehavior,
65
+ isLoading = false,
66
+ kind = 'command',
67
+ pressed,
68
+ children,
69
+ ...nativeProps
70
+ } = props
71
+ const isInteractionDisabled = disabled || isLoading
72
+ const ariaPressed = kind === 'toggle' ? pressed : undefined
73
+ const isFocusVisible = useIsFocusVisible()
74
+
75
+ const whenEnabled = useCallback<WhenEnabled>(
76
+ <T extends object>(enabledProps: T) =>
77
+ isInteractionDisabled ? {} : enabledProps,
78
+ [isInteractionDisabled],
79
+ )
80
+
81
+ const preventDisabledActivation = useCallback<PreventDisabledActivation>(
82
+ ({ onKeyDown, onKeyUp }) => {
83
+ const options = { disabledBehavior, isInteractionDisabled }
84
+ return {
85
+ onKeyDown: wrapActivationHandler('onKeyDown', options, onKeyDown),
86
+ onKeyUp: wrapActivationHandler('onKeyUp', options, onKeyUp),
87
+ }
88
+ },
89
+ [disabledBehavior, isInteractionDisabled],
90
+ )
91
+
92
+ return {
93
+ buttonProps: {
94
+ ...nativeProps,
95
+ 'aria-busy': ariaBusy ?? (isLoading ? true : undefined),
96
+ 'aria-disabled':
97
+ ariaDisabled ?? (isInteractionDisabled ? true : undefined),
98
+ 'aria-pressed': ariaPressed,
99
+ children,
100
+ className: clsx('maratus__button__button', className),
101
+ 'data-focus-visible': isFocusVisible ? '' : undefined,
102
+ 'data-loading': isLoading ? '' : undefined,
103
+ },
104
+ preventDisabledActivation,
105
+ whenEnabled,
106
+ }
107
+ }
108
+
109
+ type ActivationPhase = keyof ActivationHandlerProps
110
+ const activationKeysByPhase: Record<ActivationPhase, Set<string>> = {
111
+ onKeyDown: new Set(['Enter', ' ']),
112
+ onKeyUp: new Set([' ']),
113
+ }
114
+
115
+ function wrapActivationHandler(
116
+ phase: ActivationPhase,
117
+ options: {
118
+ disabledBehavior?: CommonButtonProps['disabledBehavior']
119
+ isInteractionDisabled: boolean
120
+ },
121
+ handler?: KeyboardEventHandler<HTMLButtonElement>,
122
+ ): KeyboardEventHandler<HTMLButtonElement> | undefined {
123
+ if (!handler && !options.isInteractionDisabled) {
124
+ return undefined
125
+ }
126
+
127
+ // Native buttons still activate from the keyboard when they remain focusable.
128
+ // Block Enter on keydown and Space on both keydown and keyup so focusable
129
+ // disabled buttons stay inert without losing focusability.
130
+ return (event) => {
131
+ const shouldPrevent =
132
+ options.isInteractionDisabled &&
133
+ options.disabledBehavior === 'focusable' &&
134
+ activationKeysByPhase[phase].has(event.key)
135
+
136
+ if (shouldPrevent) {
137
+ event.preventDefault()
138
+ return
139
+ }
140
+
141
+ handler?.(event)
142
+ }
143
+ }
@@ -0,0 +1,46 @@
1
+ import type { ButtonProps } from './useButton'
2
+ import { useButton } from './useButton'
3
+
4
+ export function Button(props: ButtonProps) {
5
+ const {
6
+ disabled,
7
+ disabledBehavior = 'native',
8
+ isLoading = false,
9
+ onClick,
10
+ onKeyDown,
11
+ onKeyUp,
12
+ onMouseDown,
13
+ onPointerDown,
14
+ onTouchStart,
15
+ type,
16
+ ...hookProps
17
+ } = props
18
+ const { buttonProps, preventDisabledActivation, whenEnabled } = useButton({
19
+ ...hookProps,
20
+ disabled,
21
+ disabledBehavior,
22
+ isLoading,
23
+ })
24
+ const { children, ...rootProps } = buttonProps
25
+ const isInteractionDisabled = disabled || isLoading
26
+
27
+ return (
28
+ <button
29
+ {...rootProps}
30
+ disabled={isInteractionDisabled && disabledBehavior === 'native'}
31
+ {...preventDisabledActivation({
32
+ onKeyDown,
33
+ onKeyUp,
34
+ })}
35
+ {...whenEnabled({
36
+ onClick,
37
+ onMouseDown,
38
+ onPointerDown,
39
+ onTouchStart,
40
+ })}
41
+ type={type}
42
+ >
43
+ {children}
44
+ </button>
45
+ )
46
+ }
@@ -0,0 +1,85 @@
1
+ :root {
2
+ --ara-button-bg: var(--ara-color-control-bg);
3
+ --ara-button-bg--disabled: var(--ara-color-control-bg--disabled);
4
+ --ara-button-bg--focus: var(--ara-color-control-bg--focus);
5
+ --ara-button-bg--hover: var(--ara-color-control-bg--hover);
6
+ --ara-button-bg--loading: var(--ara-color-control-bg--loading);
7
+ --ara-button-bg--pressed: var(--ara-color-control-bg--pressed);
8
+ --ara-button-detail: var(--ara-color-control-detail);
9
+ --ara-button-detail--disabled: var(--ara-color-control-detail--disabled);
10
+ --ara-button-detail--focus: var(--ara-color-control-detail--focus);
11
+ --ara-button-detail--hover: var(--ara-color-control-detail--hover);
12
+ --ara-button-detail--loading: var(--ara-color-control-detail--loading);
13
+ --ara-button-detail--pressed: var(--ara-color-control-detail--pressed);
14
+ --ara-button-border-radius: var(--ara-radius-x1);
15
+ --ara-button-border-width: var(--ara-border-width-x1);
16
+ --ara-button-focus: var(--ara-color-control-focus);
17
+ --ara-button-focus-offset: var(--ara-spacing-x1);
18
+ --ara-button-focus-width: var(--ara-border-width-x1);
19
+ --ara-button-padding-block: var(--ara-spacing-x1);
20
+ --ara-button-padding-inline: var(--ara-spacing-x1);
21
+ --ara-button-fg: var(--ara-color-control-fg);
22
+ --ara-button-fg--disabled: var(--ara-color-control-fg--disabled);
23
+ --ara-button-fg--focus: var(--ara-color-control-fg--focus);
24
+ --ara-button-fg--hover: var(--ara-color-control-fg--hover);
25
+ --ara-button-fg--loading: var(--ara-color-control-fg--loading);
26
+ --ara-button-fg--pressed: var(--ara-color-control-fg--pressed);
27
+ --ara-button-shadow: var(--ara-shadow-control);
28
+ --ara-button-shadow--disabled: var(--ara-shadow-control--disabled);
29
+ --ara-button-shadow--focus: var(--ara-shadow-control--focus);
30
+ --ara-button-shadow--hover: var(--ara-shadow-control--hover);
31
+ --ara-button-shadow--loading: var(--ara-shadow-control--loading);
32
+ --ara-button-shadow--pressed: var(--ara-shadow-control--pressed);
33
+ }
34
+
35
+ .button {
36
+ background-color: var(--ara-button-bg);
37
+ border-color: var(--ara-button-detail);
38
+ border-radius: var(--ara-button-border-radius);
39
+ border-width: var(--ara-button-border-width);
40
+ box-shadow: var(--ara-button-shadow);
41
+ color: var(--ara-button-fg);
42
+ padding-block: var(--ara-button-padding-block);
43
+ padding-inline: var(--ara-button-padding-inline);
44
+ }
45
+
46
+ .button:hover {
47
+ background-color: var(--ara-button-bg--hover);
48
+ border-color: var(--ara-button-detail--hover);
49
+ box-shadow: var(--ara-button-shadow--hover);
50
+ color: var(--ara-button-fg--hover);
51
+ }
52
+
53
+ .button:focus-visible,
54
+ .button[data-focus-visible] {
55
+ background-color: var(--ara-button-bg--focus);
56
+ border-color: var(--ara-button-detail--focus);
57
+ box-shadow: var(--ara-button-shadow--focus);
58
+ color: var(--ara-button-fg--focus);
59
+ outline-color: var(--ara-button-focus);
60
+ outline-offset: var(--ara-button-focus-offset);
61
+ outline-width: var(--ara-button-focus-width);
62
+ }
63
+
64
+ .button[aria-pressed='true'],
65
+ .button[aria-pressed='mixed'] {
66
+ background-color: var(--ara-button-bg--pressed);
67
+ border-color: var(--ara-button-detail--pressed);
68
+ box-shadow: var(--ara-button-shadow--pressed);
69
+ color: var(--ara-button-fg--pressed);
70
+ }
71
+
72
+ .button:disabled,
73
+ .button[aria-disabled='true'] {
74
+ background-color: var(--ara-button-bg--disabled);
75
+ border-color: var(--ara-button-detail--disabled);
76
+ box-shadow: var(--ara-button-shadow--disabled);
77
+ color: var(--ara-button-fg--disabled);
78
+ }
79
+
80
+ .button[data-loading] {
81
+ background-color: var(--ara-button-bg--loading);
82
+ border-color: var(--ara-button-detail--loading);
83
+ box-shadow: var(--ara-button-shadow--loading);
84
+ color: var(--ara-button-fg--loading);
85
+ }
@@ -0,0 +1,3 @@
1
+ export type { ButtonProps, UseButtonResult } from './useButton'
2
+ export { Button } from './Button'
3
+ export { useButton } from './useButton'
@@ -0,0 +1,143 @@
1
+ import type { ButtonHTMLAttributes, KeyboardEventHandler } from 'react'
2
+ import { useIsFocusVisible } from '@maratus-lib/focus-modality'
3
+ import clsx from 'clsx'
4
+ import { useCallback } from 'react'
5
+ import styles from './button.module.css'
6
+
7
+ type NativeButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
8
+
9
+ type CommonButtonProps = Omit<NativeButtonProps, 'aria-pressed' | 'role'> & {
10
+ disabledBehavior?: 'native' | 'focusable'
11
+ isLoading?: boolean
12
+ }
13
+
14
+ type CommandButtonProps = {
15
+ kind?: 'command'
16
+ pressed?: never
17
+ }
18
+
19
+ type ToggleButtonProps = {
20
+ kind: 'toggle'
21
+ pressed: boolean | 'mixed'
22
+ }
23
+
24
+ export type ButtonProps = CommonButtonProps &
25
+ (CommandButtonProps | ToggleButtonProps)
26
+
27
+ type ButtonRootProps = NativeButtonProps & {
28
+ 'data-focus-visible'?: ''
29
+ 'data-loading'?: ''
30
+ }
31
+
32
+ type WhenEnabled = <T extends {}>(props: T) => T | {}
33
+
34
+ type ActivationHandlerProps = {
35
+ onKeyDown?: KeyboardEventHandler<HTMLButtonElement>
36
+ onKeyUp?: KeyboardEventHandler<HTMLButtonElement>
37
+ }
38
+
39
+ type PreventDisabledActivation = (
40
+ props: ActivationHandlerProps,
41
+ ) => ActivationHandlerProps
42
+
43
+ export type UseButtonResult = {
44
+ buttonProps: ButtonRootProps
45
+
46
+ /**
47
+ * Helper function that suppresses props when the button is disabled
48
+ */
49
+ whenEnabled: WhenEnabled
50
+
51
+ /**
52
+ * Helper function that prevents keyboard-triggered activation while the
53
+ * button remains focusable.
54
+ */
55
+ preventDisabledActivation: PreventDisabledActivation
56
+ }
57
+
58
+ export function useButton(props: ButtonProps): UseButtonResult {
59
+ const {
60
+ 'aria-busy': ariaBusy,
61
+ 'aria-disabled': ariaDisabled,
62
+ className,
63
+ disabled,
64
+ disabledBehavior,
65
+ isLoading = false,
66
+ kind = 'command',
67
+ pressed,
68
+ children,
69
+ ...nativeProps
70
+ } = props
71
+ const isInteractionDisabled = disabled || isLoading
72
+ const ariaPressed = kind === 'toggle' ? pressed : undefined
73
+ const isFocusVisible = useIsFocusVisible()
74
+
75
+ const whenEnabled = useCallback<WhenEnabled>(
76
+ <T extends object>(enabledProps: T) =>
77
+ isInteractionDisabled ? {} : enabledProps,
78
+ [isInteractionDisabled],
79
+ )
80
+
81
+ const preventDisabledActivation = useCallback<PreventDisabledActivation>(
82
+ ({ onKeyDown, onKeyUp }) => {
83
+ const options = { disabledBehavior, isInteractionDisabled }
84
+ return {
85
+ onKeyDown: wrapActivationHandler('onKeyDown', options, onKeyDown),
86
+ onKeyUp: wrapActivationHandler('onKeyUp', options, onKeyUp),
87
+ }
88
+ },
89
+ [disabledBehavior, isInteractionDisabled],
90
+ )
91
+
92
+ return {
93
+ buttonProps: {
94
+ ...nativeProps,
95
+ 'aria-busy': ariaBusy ?? (isLoading ? true : undefined),
96
+ 'aria-disabled':
97
+ ariaDisabled ?? (isInteractionDisabled ? true : undefined),
98
+ 'aria-pressed': ariaPressed,
99
+ children,
100
+ className: clsx(styles.button, className),
101
+ 'data-focus-visible': isFocusVisible ? '' : undefined,
102
+ 'data-loading': isLoading ? '' : undefined,
103
+ },
104
+ preventDisabledActivation,
105
+ whenEnabled,
106
+ }
107
+ }
108
+
109
+ type ActivationPhase = keyof ActivationHandlerProps
110
+ const activationKeysByPhase: Record<ActivationPhase, Set<string>> = {
111
+ onKeyDown: new Set(['Enter', ' ']),
112
+ onKeyUp: new Set([' ']),
113
+ }
114
+
115
+ function wrapActivationHandler(
116
+ phase: ActivationPhase,
117
+ options: {
118
+ disabledBehavior?: CommonButtonProps['disabledBehavior']
119
+ isInteractionDisabled: boolean
120
+ },
121
+ handler?: KeyboardEventHandler<HTMLButtonElement>,
122
+ ): KeyboardEventHandler<HTMLButtonElement> | undefined {
123
+ if (!handler && !options.isInteractionDisabled) {
124
+ return undefined
125
+ }
126
+
127
+ // Native buttons still activate from the keyboard when they remain focusable.
128
+ // Block Enter on keydown and Space on both keydown and keyup so focusable
129
+ // disabled buttons stay inert without losing focusability.
130
+ return (event) => {
131
+ const shouldPrevent =
132
+ options.isInteractionDisabled &&
133
+ options.disabledBehavior === 'focusable' &&
134
+ activationKeysByPhase[phase].has(event.key)
135
+
136
+ if (shouldPrevent) {
137
+ event.preventDefault()
138
+ return
139
+ }
140
+
141
+ handler?.(event)
142
+ }
143
+ }
package/meta.json ADDED
@@ -0,0 +1,158 @@
1
+ {
2
+ "themeTokens": [
3
+ "--ara-color-control-bg",
4
+ "--ara-color-control-bg--disabled",
5
+ "--ara-color-control-bg--focus",
6
+ "--ara-color-control-bg--hover",
7
+ "--ara-color-control-bg--loading",
8
+ "--ara-color-control-bg--pressed",
9
+ "--ara-color-control-detail",
10
+ "--ara-color-control-detail--disabled",
11
+ "--ara-color-control-detail--focus",
12
+ "--ara-color-control-detail--hover",
13
+ "--ara-color-control-detail--loading",
14
+ "--ara-color-control-detail--pressed",
15
+ "--ara-radius-x1",
16
+ "--ara-border-width-x1",
17
+ "--ara-color-control-focus",
18
+ "--ara-spacing-x1",
19
+ "--ara-color-control-fg",
20
+ "--ara-color-control-fg--disabled",
21
+ "--ara-color-control-fg--focus",
22
+ "--ara-color-control-fg--hover",
23
+ "--ara-color-control-fg--loading",
24
+ "--ara-color-control-fg--pressed",
25
+ "--ara-shadow-control",
26
+ "--ara-shadow-control--disabled",
27
+ "--ara-shadow-control--focus",
28
+ "--ara-shadow-control--hover",
29
+ "--ara-shadow-control--loading",
30
+ "--ara-shadow-control--pressed"
31
+ ],
32
+ "componentTokens": [
33
+ {
34
+ "component": "--ara-button-bg",
35
+ "theme": "--ara-color-control-bg"
36
+ },
37
+ {
38
+ "component": "--ara-button-bg--disabled",
39
+ "theme": "--ara-color-control-bg--disabled"
40
+ },
41
+ {
42
+ "component": "--ara-button-bg--focus",
43
+ "theme": "--ara-color-control-bg--focus"
44
+ },
45
+ {
46
+ "component": "--ara-button-bg--hover",
47
+ "theme": "--ara-color-control-bg--hover"
48
+ },
49
+ {
50
+ "component": "--ara-button-bg--loading",
51
+ "theme": "--ara-color-control-bg--loading"
52
+ },
53
+ {
54
+ "component": "--ara-button-bg--pressed",
55
+ "theme": "--ara-color-control-bg--pressed"
56
+ },
57
+ {
58
+ "component": "--ara-button-detail",
59
+ "theme": "--ara-color-control-detail"
60
+ },
61
+ {
62
+ "component": "--ara-button-detail--disabled",
63
+ "theme": "--ara-color-control-detail--disabled"
64
+ },
65
+ {
66
+ "component": "--ara-button-detail--focus",
67
+ "theme": "--ara-color-control-detail--focus"
68
+ },
69
+ {
70
+ "component": "--ara-button-detail--hover",
71
+ "theme": "--ara-color-control-detail--hover"
72
+ },
73
+ {
74
+ "component": "--ara-button-detail--loading",
75
+ "theme": "--ara-color-control-detail--loading"
76
+ },
77
+ {
78
+ "component": "--ara-button-detail--pressed",
79
+ "theme": "--ara-color-control-detail--pressed"
80
+ },
81
+ {
82
+ "component": "--ara-button-border-radius",
83
+ "theme": "--ara-radius-x1"
84
+ },
85
+ {
86
+ "component": "--ara-button-border-width",
87
+ "theme": "--ara-border-width-x1"
88
+ },
89
+ {
90
+ "component": "--ara-button-focus",
91
+ "theme": "--ara-color-control-focus"
92
+ },
93
+ {
94
+ "component": "--ara-button-focus-offset",
95
+ "theme": "--ara-spacing-x1"
96
+ },
97
+ {
98
+ "component": "--ara-button-focus-width",
99
+ "theme": "--ara-border-width-x1"
100
+ },
101
+ {
102
+ "component": "--ara-button-padding-block",
103
+ "theme": "--ara-spacing-x1"
104
+ },
105
+ {
106
+ "component": "--ara-button-padding-inline",
107
+ "theme": "--ara-spacing-x1"
108
+ },
109
+ {
110
+ "component": "--ara-button-fg",
111
+ "theme": "--ara-color-control-fg"
112
+ },
113
+ {
114
+ "component": "--ara-button-fg--disabled",
115
+ "theme": "--ara-color-control-fg--disabled"
116
+ },
117
+ {
118
+ "component": "--ara-button-fg--focus",
119
+ "theme": "--ara-color-control-fg--focus"
120
+ },
121
+ {
122
+ "component": "--ara-button-fg--hover",
123
+ "theme": "--ara-color-control-fg--hover"
124
+ },
125
+ {
126
+ "component": "--ara-button-fg--loading",
127
+ "theme": "--ara-color-control-fg--loading"
128
+ },
129
+ {
130
+ "component": "--ara-button-fg--pressed",
131
+ "theme": "--ara-color-control-fg--pressed"
132
+ },
133
+ {
134
+ "component": "--ara-button-shadow",
135
+ "theme": "--ara-shadow-control"
136
+ },
137
+ {
138
+ "component": "--ara-button-shadow--disabled",
139
+ "theme": "--ara-shadow-control--disabled"
140
+ },
141
+ {
142
+ "component": "--ara-button-shadow--focus",
143
+ "theme": "--ara-shadow-control--focus"
144
+ },
145
+ {
146
+ "component": "--ara-button-shadow--hover",
147
+ "theme": "--ara-shadow-control--hover"
148
+ },
149
+ {
150
+ "component": "--ara-button-shadow--loading",
151
+ "theme": "--ara-shadow-control--loading"
152
+ },
153
+ {
154
+ "component": "--ara-button-shadow--pressed",
155
+ "theme": "--ara-shadow-control--pressed"
156
+ }
157
+ ]
158
+ }
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@maratus-registry/button",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
+ "files": [
6
+ "css-files",
7
+ "css-modules",
8
+ "tailwind-css",
9
+ "meta.json"
10
+ ],
5
11
  "type": "module",
6
12
  "dependencies": {
7
- "@maratus/focus-modality": "workspace:*"
13
+ "@maratus-lib/focus-modality": "workspace:*"
8
14
  },
9
15
  "peerDependencies": {
10
16
  "clsx": "^2.1.1",
@@ -0,0 +1,46 @@
1
+ import type { ButtonProps } from './useButton'
2
+ import { useButton } from './useButton'
3
+
4
+ export function Button(props: ButtonProps) {
5
+ const {
6
+ disabled,
7
+ disabledBehavior = 'native',
8
+ isLoading = false,
9
+ onClick,
10
+ onKeyDown,
11
+ onKeyUp,
12
+ onMouseDown,
13
+ onPointerDown,
14
+ onTouchStart,
15
+ type,
16
+ ...hookProps
17
+ } = props
18
+ const { buttonProps, preventDisabledActivation, whenEnabled } = useButton({
19
+ ...hookProps,
20
+ disabled,
21
+ disabledBehavior,
22
+ isLoading,
23
+ })
24
+ const { children, ...rootProps } = buttonProps
25
+ const isInteractionDisabled = disabled || isLoading
26
+
27
+ return (
28
+ <button
29
+ {...rootProps}
30
+ disabled={isInteractionDisabled && disabledBehavior === 'native'}
31
+ {...preventDisabledActivation({
32
+ onKeyDown,
33
+ onKeyUp,
34
+ })}
35
+ {...whenEnabled({
36
+ onClick,
37
+ onMouseDown,
38
+ onPointerDown,
39
+ onTouchStart,
40
+ })}
41
+ type={type}
42
+ >
43
+ {children}
44
+ </button>
45
+ )
46
+ }
@@ -0,0 +1,89 @@
1
+ @reference "tailwindcss";
2
+
3
+ :root {
4
+ --ara-button-bg: var(--ara-color-control-bg);
5
+ --ara-button-bg--disabled: var(--ara-color-control-bg--disabled);
6
+ --ara-button-bg--focus: var(--ara-color-control-bg--focus);
7
+ --ara-button-bg--hover: var(--ara-color-control-bg--hover);
8
+ --ara-button-bg--loading: var(--ara-color-control-bg--loading);
9
+ --ara-button-bg--pressed: var(--ara-color-control-bg--pressed);
10
+ --ara-button-detail: var(--ara-color-control-detail);
11
+ --ara-button-detail--disabled: var(--ara-color-control-detail--disabled);
12
+ --ara-button-detail--focus: var(--ara-color-control-detail--focus);
13
+ --ara-button-detail--hover: var(--ara-color-control-detail--hover);
14
+ --ara-button-detail--loading: var(--ara-color-control-detail--loading);
15
+ --ara-button-detail--pressed: var(--ara-color-control-detail--pressed);
16
+ --ara-button-border-radius: var(--ara-radius-x1);
17
+ --ara-button-border-width: var(--ara-border-width-x1);
18
+ --ara-button-focus: var(--ara-color-control-focus);
19
+ --ara-button-focus-offset: var(--ara-spacing-x1);
20
+ --ara-button-focus-width: var(--ara-border-width-x1);
21
+ --ara-button-padding-block: var(--ara-spacing-x1);
22
+ --ara-button-padding-inline: var(--ara-spacing-x1);
23
+ --ara-button-fg: var(--ara-color-control-fg);
24
+ --ara-button-fg--disabled: var(--ara-color-control-fg--disabled);
25
+ --ara-button-fg--focus: var(--ara-color-control-fg--focus);
26
+ --ara-button-fg--hover: var(--ara-color-control-fg--hover);
27
+ --ara-button-fg--loading: var(--ara-color-control-fg--loading);
28
+ --ara-button-fg--pressed: var(--ara-color-control-fg--pressed);
29
+ --ara-button-shadow: var(--ara-shadow-control);
30
+ --ara-button-shadow--disabled: var(--ara-shadow-control--disabled);
31
+ --ara-button-shadow--focus: var(--ara-shadow-control--focus);
32
+ --ara-button-shadow--hover: var(--ara-shadow-control--hover);
33
+ --ara-button-shadow--loading: var(--ara-shadow-control--loading);
34
+ --ara-button-shadow--pressed: var(--ara-shadow-control--pressed);
35
+ }
36
+
37
+ @layer components {
38
+ .maratus__button__button {
39
+ background-color: var(--ara-button-bg);
40
+ border-color: var(--ara-button-detail);
41
+ border-radius: var(--ara-button-border-radius);
42
+ border-width: var(--ara-button-border-width);
43
+ box-shadow: var(--ara-button-shadow);
44
+ color: var(--ara-button-fg);
45
+ padding-block: var(--ara-button-padding-block);
46
+ padding-inline: var(--ara-button-padding-inline);
47
+ }
48
+
49
+ .maratus__button__button:hover {
50
+ background-color: var(--ara-button-bg--hover);
51
+ border-color: var(--ara-button-detail--hover);
52
+ box-shadow: var(--ara-button-shadow--hover);
53
+ color: var(--ara-button-fg--hover);
54
+ }
55
+
56
+ .maratus__button__button:focus-visible,
57
+ .maratus__button__button[data-focus-visible] {
58
+ background-color: var(--ara-button-bg--focus);
59
+ border-color: var(--ara-button-detail--focus);
60
+ box-shadow: var(--ara-button-shadow--focus);
61
+ color: var(--ara-button-fg--focus);
62
+ outline-color: var(--ara-button-focus);
63
+ outline-offset: var(--ara-button-focus-offset);
64
+ outline-width: var(--ara-button-focus-width);
65
+ }
66
+
67
+ .maratus__button__button[aria-pressed='true'],
68
+ .maratus__button__button[aria-pressed='mixed'] {
69
+ background-color: var(--ara-button-bg--pressed);
70
+ border-color: var(--ara-button-detail--pressed);
71
+ box-shadow: var(--ara-button-shadow--pressed);
72
+ color: var(--ara-button-fg--pressed);
73
+ }
74
+
75
+ .maratus__button__button:disabled,
76
+ .maratus__button__button[aria-disabled='true'] {
77
+ background-color: var(--ara-button-bg--disabled);
78
+ border-color: var(--ara-button-detail--disabled);
79
+ box-shadow: var(--ara-button-shadow--disabled);
80
+ color: var(--ara-button-fg--disabled);
81
+ }
82
+
83
+ .maratus__button__button[data-loading] {
84
+ background-color: var(--ara-button-bg--loading);
85
+ border-color: var(--ara-button-detail--loading);
86
+ box-shadow: var(--ara-button-shadow--loading);
87
+ color: var(--ara-button-fg--loading);
88
+ }
89
+ }
@@ -0,0 +1,3 @@
1
+ export type { ButtonProps, UseButtonResult } from './useButton'
2
+ export { Button } from './Button'
3
+ export { useButton } from './useButton'
@@ -0,0 +1,143 @@
1
+ import type { ButtonHTMLAttributes, KeyboardEventHandler } from 'react'
2
+ import { useIsFocusVisible } from '@maratus-lib/focus-modality'
3
+ import clsx from 'clsx'
4
+ import { useCallback } from 'react'
5
+ import './button.css'
6
+
7
+ type NativeButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
8
+
9
+ type CommonButtonProps = Omit<NativeButtonProps, 'aria-pressed' | 'role'> & {
10
+ disabledBehavior?: 'native' | 'focusable'
11
+ isLoading?: boolean
12
+ }
13
+
14
+ type CommandButtonProps = {
15
+ kind?: 'command'
16
+ pressed?: never
17
+ }
18
+
19
+ type ToggleButtonProps = {
20
+ kind: 'toggle'
21
+ pressed: boolean | 'mixed'
22
+ }
23
+
24
+ export type ButtonProps = CommonButtonProps &
25
+ (CommandButtonProps | ToggleButtonProps)
26
+
27
+ type ButtonRootProps = NativeButtonProps & {
28
+ 'data-focus-visible'?: ''
29
+ 'data-loading'?: ''
30
+ }
31
+
32
+ type WhenEnabled = <T extends {}>(props: T) => T | {}
33
+
34
+ type ActivationHandlerProps = {
35
+ onKeyDown?: KeyboardEventHandler<HTMLButtonElement>
36
+ onKeyUp?: KeyboardEventHandler<HTMLButtonElement>
37
+ }
38
+
39
+ type PreventDisabledActivation = (
40
+ props: ActivationHandlerProps,
41
+ ) => ActivationHandlerProps
42
+
43
+ export type UseButtonResult = {
44
+ buttonProps: ButtonRootProps
45
+
46
+ /**
47
+ * Helper function that suppresses props when the button is disabled
48
+ */
49
+ whenEnabled: WhenEnabled
50
+
51
+ /**
52
+ * Helper function that prevents keyboard-triggered activation while the
53
+ * button remains focusable.
54
+ */
55
+ preventDisabledActivation: PreventDisabledActivation
56
+ }
57
+
58
+ export function useButton(props: ButtonProps): UseButtonResult {
59
+ const {
60
+ 'aria-busy': ariaBusy,
61
+ 'aria-disabled': ariaDisabled,
62
+ className,
63
+ disabled,
64
+ disabledBehavior,
65
+ isLoading = false,
66
+ kind = 'command',
67
+ pressed,
68
+ children,
69
+ ...nativeProps
70
+ } = props
71
+ const isInteractionDisabled = disabled || isLoading
72
+ const ariaPressed = kind === 'toggle' ? pressed : undefined
73
+ const isFocusVisible = useIsFocusVisible()
74
+
75
+ const whenEnabled = useCallback<WhenEnabled>(
76
+ <T extends object>(enabledProps: T) =>
77
+ isInteractionDisabled ? {} : enabledProps,
78
+ [isInteractionDisabled],
79
+ )
80
+
81
+ const preventDisabledActivation = useCallback<PreventDisabledActivation>(
82
+ ({ onKeyDown, onKeyUp }) => {
83
+ const options = { disabledBehavior, isInteractionDisabled }
84
+ return {
85
+ onKeyDown: wrapActivationHandler('onKeyDown', options, onKeyDown),
86
+ onKeyUp: wrapActivationHandler('onKeyUp', options, onKeyUp),
87
+ }
88
+ },
89
+ [disabledBehavior, isInteractionDisabled],
90
+ )
91
+
92
+ return {
93
+ buttonProps: {
94
+ ...nativeProps,
95
+ 'aria-busy': ariaBusy ?? (isLoading ? true : undefined),
96
+ 'aria-disabled':
97
+ ariaDisabled ?? (isInteractionDisabled ? true : undefined),
98
+ 'aria-pressed': ariaPressed,
99
+ children,
100
+ className: clsx('maratus__button__button', className),
101
+ 'data-focus-visible': isFocusVisible ? '' : undefined,
102
+ 'data-loading': isLoading ? '' : undefined,
103
+ },
104
+ preventDisabledActivation,
105
+ whenEnabled,
106
+ }
107
+ }
108
+
109
+ type ActivationPhase = keyof ActivationHandlerProps
110
+ const activationKeysByPhase: Record<ActivationPhase, Set<string>> = {
111
+ onKeyDown: new Set(['Enter', ' ']),
112
+ onKeyUp: new Set([' ']),
113
+ }
114
+
115
+ function wrapActivationHandler(
116
+ phase: ActivationPhase,
117
+ options: {
118
+ disabledBehavior?: CommonButtonProps['disabledBehavior']
119
+ isInteractionDisabled: boolean
120
+ },
121
+ handler?: KeyboardEventHandler<HTMLButtonElement>,
122
+ ): KeyboardEventHandler<HTMLButtonElement> | undefined {
123
+ if (!handler && !options.isInteractionDisabled) {
124
+ return undefined
125
+ }
126
+
127
+ // Native buttons still activate from the keyboard when they remain focusable.
128
+ // Block Enter on keydown and Space on both keydown and keyup so focusable
129
+ // disabled buttons stay inert without losing focusability.
130
+ return (event) => {
131
+ const shouldPrevent =
132
+ options.isInteractionDisabled &&
133
+ options.disabledBehavior === 'focusable' &&
134
+ activationKeysByPhase[phase].has(event.key)
135
+
136
+ if (shouldPrevent) {
137
+ event.preventDefault()
138
+ return
139
+ }
140
+
141
+ handler?.(event)
142
+ }
143
+ }
package/CHANGELOG.md DELETED
@@ -1,12 +0,0 @@
1
- # @maratus-registry/button
2
-
3
- ## 0.1.0
4
-
5
- ### Minor Changes
6
-
7
- - 479324d: Publish the initial public Maratus packages, including the CLI, platform binaries, codemod runner, codemods, runtime libraries, and the first registry components: separator, button, focus-modality.
8
-
9
- ### Patch Changes
10
-
11
- - Updated dependencies [479324d]
12
- - @maratus/focus-modality@0.1.0