@mindlogic-ai/logician-ui 5.0.0-alpha.19 → 5.0.0-alpha.20

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 +1 @@
1
- {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/Button/Button.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAA8B,MAAM,gBAAgB,CAAC;AA0K5E,eAAO,MAAM,MAAM,gEAA0C,CAAC"}
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/Button/Button.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAA8B,MAAM,gBAAgB,CAAC;AAwM5E,eAAO,MAAM,MAAM,gEAA0C,CAAC"}
@@ -28,7 +28,7 @@ var Button_styles = require('./Button.styles.js');
28
28
  * <Button as="a" href="/docs" target="_blank">문서</Button>
29
29
  * ```
30
30
  */
31
- const ButtonImpl = React.forwardRef(({ variant: variantProp, isDestructive: isDestructiveProp = false, size, lift, children, _hover: hoverProp, _active: activeProp, _disabled: disabledProp, css: cssProp, ...restWithLegacy }, ref) => {
31
+ const ButtonImpl = React.forwardRef(({ variant: variantProp, isDestructive: isDestructiveProp = false, size, lift, disabled, focusableWhenDisabled = false, onDisabledClick, onClick, children, _hover: hoverProp, _active: activeProp, _disabled: disabledProp, css: cssProp, ...restWithLegacy }, ref) => {
32
32
  // 4.x callers (untyped JS, `as any`) can still hand us `colorPalette` and
33
33
  // an old variant name. Strip it so it never reaches Chakra's own recipe,
34
34
  // and map the pair onto a 5.0 role — see `Button.legacy.ts`.
@@ -49,6 +49,23 @@ const ButtonImpl = React.forwardRef(({ variant: variantProp, isDestructive: isDe
49
49
  isDestructive = isDestructive || resolved.isDestructive;
50
50
  }
51
51
  const base = Button_styles.getButtonStyles(variant, isDestructive);
52
+ // `focusableWhenDisabled`: disabled by ARIA, not by the attribute, so the
53
+ // button stays focusable and can answer a press. `aria-disabled=true` is
54
+ // in Chakra's `_disabled` condition, so the disabled colours and cursor
55
+ // still apply; `data-disabled` is what `_hover` / `_active` exclude, so
56
+ // without it the button would still react to the pointer. `loading` keeps
57
+ // the native attribute (Chakra sets it) — see `focusableWhenDisabled`.
58
+ const softDisabled = !!disabled && focusableWhenDisabled && !rest.loading;
59
+ const handleClick = softDisabled
60
+ ? (event) => {
61
+ // Also what stops a `type="submit"` button from submitting.
62
+ event.preventDefault();
63
+ // A native disabled button sends no click to its ancestors either —
64
+ // a clickable row around it must not run its own action.
65
+ event.stopPropagation();
66
+ onDisabledClick?.(event);
67
+ }
68
+ : onClick;
52
69
  // Merged rather than spread after: each variant already owns `_hover` and
53
70
  // `_active` for its colours, and replacing either would drop them.
54
71
  const styles = lift
@@ -112,7 +129,10 @@ const ButtonImpl = React.forwardRef(({ variant: variantProp, isDestructive: isDe
112
129
  // Two clocks on one element — the press is faster than the colour
113
130
  // change — so the shorthand is written out and only the reduced-motion
114
131
  // guard comes from the vocabulary.
115
- transition: Button_styles.buttonTransition, animationStyle: "composite", ref: ref, "data-variant": variant, "data-destructive": isDestructive || undefined, loadingText: loadingText, ...rest,
132
+ transition: Button_styles.buttonTransition, animationStyle: "composite", ref: ref, "data-variant": variant, "data-destructive": isDestructive || undefined, loadingText: loadingText, disabled: softDisabled ? undefined : disabled, onClick: handleClick, ...rest, ...(softDisabled && {
133
+ 'aria-disabled': true,
134
+ 'data-disabled': '',
135
+ }),
116
136
  // The `action` icon rule lives in `css` (a nested selector cannot be a
117
137
  // style prop); merged so a call site's `css` adds to it.
118
138
  css: mergeCss.mergeCss(base.css, cssProp), children: children }));
@@ -1 +1 @@
1
- {"version":3,"file":"Button.js","sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":[null],"names":["forwardRef","useTranslate","isLegacyButtonProps","resolveLegacyButtonVariant","warnLegacyButtonProps","getButtonStyles","_jsx","ChakraButton","mergeCss","focusRing","buttonTransition","polymorphic"],"mappings":"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"}
1
+ {"version":3,"file":"Button.js","sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":[null],"names":["forwardRef","useTranslate","isLegacyButtonProps","resolveLegacyButtonVariant","warnLegacyButtonProps","getButtonStyles","_jsx","ChakraButton","mergeCss","focusRing","buttonTransition","polymorphic"],"mappings":"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"}
@@ -26,7 +26,7 @@ import { getButtonStyles, buttonTransition } from './Button.styles.mjs';
26
26
  * <Button as="a" href="/docs" target="_blank">문서</Button>
27
27
  * ```
28
28
  */
29
- const ButtonImpl = forwardRef(({ variant: variantProp, isDestructive: isDestructiveProp = false, size, lift, children, _hover: hoverProp, _active: activeProp, _disabled: disabledProp, css: cssProp, ...restWithLegacy }, ref) => {
29
+ const ButtonImpl = forwardRef(({ variant: variantProp, isDestructive: isDestructiveProp = false, size, lift, disabled, focusableWhenDisabled = false, onDisabledClick, onClick, children, _hover: hoverProp, _active: activeProp, _disabled: disabledProp, css: cssProp, ...restWithLegacy }, ref) => {
30
30
  // 4.x callers (untyped JS, `as any`) can still hand us `colorPalette` and
31
31
  // an old variant name. Strip it so it never reaches Chakra's own recipe,
32
32
  // and map the pair onto a 5.0 role — see `Button.legacy.ts`.
@@ -47,6 +47,23 @@ const ButtonImpl = forwardRef(({ variant: variantProp, isDestructive: isDestruct
47
47
  isDestructive = isDestructive || resolved.isDestructive;
48
48
  }
49
49
  const base = getButtonStyles(variant, isDestructive);
50
+ // `focusableWhenDisabled`: disabled by ARIA, not by the attribute, so the
51
+ // button stays focusable and can answer a press. `aria-disabled=true` is
52
+ // in Chakra's `_disabled` condition, so the disabled colours and cursor
53
+ // still apply; `data-disabled` is what `_hover` / `_active` exclude, so
54
+ // without it the button would still react to the pointer. `loading` keeps
55
+ // the native attribute (Chakra sets it) — see `focusableWhenDisabled`.
56
+ const softDisabled = !!disabled && focusableWhenDisabled && !rest.loading;
57
+ const handleClick = softDisabled
58
+ ? (event) => {
59
+ // Also what stops a `type="submit"` button from submitting.
60
+ event.preventDefault();
61
+ // A native disabled button sends no click to its ancestors either —
62
+ // a clickable row around it must not run its own action.
63
+ event.stopPropagation();
64
+ onDisabledClick?.(event);
65
+ }
66
+ : onClick;
50
67
  // Merged rather than spread after: each variant already owns `_hover` and
51
68
  // `_active` for its colours, and replacing either would drop them.
52
69
  const styles = lift
@@ -110,7 +127,10 @@ const ButtonImpl = forwardRef(({ variant: variantProp, isDestructive: isDestruct
110
127
  // Two clocks on one element — the press is faster than the colour
111
128
  // change — so the shorthand is written out and only the reduced-motion
112
129
  // guard comes from the vocabulary.
113
- transition: buttonTransition, animationStyle: "composite", ref: ref, "data-variant": variant, "data-destructive": isDestructive || undefined, loadingText: loadingText, ...rest,
130
+ transition: buttonTransition, animationStyle: "composite", ref: ref, "data-variant": variant, "data-destructive": isDestructive || undefined, loadingText: loadingText, disabled: softDisabled ? undefined : disabled, onClick: handleClick, ...rest, ...(softDisabled && {
131
+ 'aria-disabled': true,
132
+ 'data-disabled': '',
133
+ }),
114
134
  // The `action` icon rule lives in `css` (a nested selector cannot be a
115
135
  // style prop); merged so a call site's `css` adds to it.
116
136
  css: mergeCss(base.css, cssProp), children: children }));
@@ -1 +1 @@
1
- {"version":3,"file":"Button.mjs","sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":[null],"names":["_jsx","ChakraButton"],"mappings":"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"}
1
+ {"version":3,"file":"Button.mjs","sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":[null],"names":["_jsx","ChakraButton"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { ElementType } from 'react';
1
+ import type { ElementType, MouseEvent } from 'react';
2
2
  import { ButtonProps as ChakraButtonProps } from '@chakra-ui/react';
3
3
  import type { PolymorphicProps } from '../../types/polymorphic';
4
4
  /**
@@ -81,6 +81,43 @@ export type ButtonOwnProps = Omit<ChakraButtonProps, 'variant' | 'colorScheme' |
81
81
  * @default false
82
82
  */
83
83
  lift?: boolean;
84
+ /**
85
+ * Keeps a `disabled` button in the Tab order and lets it receive clicks, so
86
+ * the screen can say *why* it is disabled.
87
+ *
88
+ * A natively `disabled` `<button>` cannot be focused and swallows every
89
+ * click: a keyboard or screen reader user may never find it, and a mouse
90
+ * user who presses it gets no answer at all. With this prop, `disabled`
91
+ * renders as `aria-disabled="true"` (plus `data-disabled`) instead of the
92
+ * native attribute:
93
+ *
94
+ * - the disabled colours, the `not-allowed` cursor and the "no hover, no
95
+ * press" behaviour are the same as a native disabled button;
96
+ * - Tab still reaches it and the focus ring shows;
97
+ * - activating it (click, Enter, Space) does **not** call `onClick`, and its
98
+ * default action is cancelled, so a `type="submit"` button does not submit
99
+ * its form; the click does not bubble to ancestors; `onDisabledClick` is
100
+ * called instead.
101
+ *
102
+ * Only activation is blocked: other handlers (`onKeyDown`, `onPointerDown`,
103
+ * …) still run, as they would on any focusable element. Inside a trigger
104
+ * (`Menu.Trigger asChild`, `Popover.Trigger asChild`) disable the root as
105
+ * well. Meant for `<button>`: with `as="a"` the `href` stays, so middle-click
106
+ * still opens it.
107
+ *
108
+ * Has no effect unless `disabled` is set. `loading` keeps the native
109
+ * `disabled` — a busy button is working, not refusing.
110
+ *
111
+ * @default false
112
+ */
113
+ focusableWhenDisabled?: boolean;
114
+ /**
115
+ * Called when a `disabled` + `focusableWhenDisabled` button is activated
116
+ * (click, Enter, Space) — where `onClick` would have been. Use it to explain
117
+ * the reason or move focus to what is missing (an unticked consent box).
118
+ * Never called on a natively disabled button, and never while `loading`.
119
+ */
120
+ onDisabledClick?: (event: MouseEvent<HTMLButtonElement>) => void;
84
121
  };
85
122
  /**
86
123
  * Props for `Button`, typed for whatever element `as` renders.
@@ -1 +1 @@
1
- {"version":3,"file":"Button.types.d.ts","sourceRoot":"","sources":["../../../src/components/Button/Button.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACzC,OAAO,EAAE,WAAW,IAAI,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAOpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,QAAQ,GACR,OAAO,GACP,QAAQ,CAAC;AAEb;;;;;;;;;GASG;AACH,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,WAAW,GACX,QAAQ,GACR,SAAS,GACT,SAAS,GACT,SAAS,CAAC;AAEd;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEzE;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,iBAAiB,EACjB,SAAS,GAAG,aAAa,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,IAAI,CAC7E,GAAG;IACF;;;;OAIG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,WAAW,GAAG,QAAQ,IAC7D,gBAAgB,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC"}
1
+ {"version":3,"file":"Button.types.d.ts","sourceRoot":"","sources":["../../../src/components/Button/Button.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,WAAW,IAAI,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAOpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,QAAQ,GACR,OAAO,GACP,QAAQ,CAAC;AAEb;;;;;;;;;GASG;AACH,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,WAAW,GACX,QAAQ,GACR,SAAS,GACT,SAAS,GACT,SAAS,CAAC;AAEd;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEzE;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,iBAAiB,EACjB,SAAS,GAAG,aAAa,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,IAAI,CAC7E,GAAG;IACF;;;;OAIG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;CAClE,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,WAAW,GAAG,QAAQ,IAC7D,gBAAgB,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindlogic-ai/logician-ui",
3
- "version": "5.0.0-alpha.19",
3
+ "version": "5.0.0-alpha.20",
4
4
  "description": "A comprehensive React design system built on Chakra UI",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -0,0 +1,218 @@
1
+ import { ChakraProvider } from '@chakra-ui/react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+
6
+ import { system } from '../../theme';
7
+ import { Button } from './Button';
8
+
9
+ /**
10
+ * `focusableWhenDisabled` — a disabled button that can still be reached and
11
+ * pressed, so the screen can say why it is disabled.
12
+ *
13
+ * A native `disabled` button leaves the Tab order and swallows every click.
14
+ * FactChat's checkout pay button sat locked until a consent box was ticked,
15
+ * and pressing it did nothing at all — the reason was off screen. With the
16
+ * prop the button is disabled by `aria-disabled` instead: it looks and refuses
17
+ * the same, but focus and a press reach it, and `onDisabledClick` answers.
18
+ */
19
+ const wrap = (ui: React.ReactNode) =>
20
+ render(<ChakraProvider value={system}>{ui}</ChakraProvider>);
21
+
22
+ describe('Button focusableWhenDisabled', () => {
23
+ it('drops native disabled for aria-disabled and data-disabled', () => {
24
+ wrap(
25
+ <Button disabled focusableWhenDisabled>
26
+ 결제
27
+ </Button>
28
+ );
29
+ const button = screen.getByRole('button', { name: '결제' });
30
+ expect(button).not.toHaveAttribute('disabled');
31
+ expect(button).toHaveAttribute('aria-disabled', 'true');
32
+ expect(button).toHaveAttribute('data-disabled');
33
+ });
34
+
35
+ it('keeps the disabled look and cursor', () => {
36
+ wrap(
37
+ <>
38
+ <Button variant="primary" disabled>
39
+ native
40
+ </Button>
41
+ <Button variant="primary" disabled focusableWhenDisabled>
42
+ soft
43
+ </Button>
44
+ </>
45
+ );
46
+ const native = getComputedStyle(
47
+ screen.getByRole('button', { name: 'native' })
48
+ );
49
+ const soft = getComputedStyle(screen.getByRole('button', { name: 'soft' }));
50
+ expect(soft.cursor).toBe('not-allowed');
51
+ expect(soft.backgroundColor).toBe(native.backgroundColor);
52
+ expect(soft.color).toBe(native.color);
53
+ });
54
+
55
+ it('stays in the Tab order', async () => {
56
+ const user = userEvent.setup();
57
+ wrap(
58
+ <>
59
+ <Button>before</Button>
60
+ <Button disabled focusableWhenDisabled>
61
+ 결제
62
+ </Button>
63
+ </>
64
+ );
65
+ await user.tab();
66
+ await user.tab();
67
+ expect(screen.getByRole('button', { name: '결제' })).toHaveFocus();
68
+ });
69
+
70
+ it('click, Enter and Space call onDisabledClick, never onClick', async () => {
71
+ const user = userEvent.setup();
72
+ const onClick = vi.fn();
73
+ const onDisabledClick = vi.fn();
74
+ wrap(
75
+ <Button
76
+ disabled
77
+ focusableWhenDisabled
78
+ onClick={onClick}
79
+ onDisabledClick={onDisabledClick}
80
+ >
81
+ 결제
82
+ </Button>
83
+ );
84
+ const button = screen.getByRole('button', { name: '결제' });
85
+ await user.click(button);
86
+ button.focus();
87
+ await user.keyboard('{Enter}');
88
+ await user.keyboard(' ');
89
+ expect(onClick).not.toHaveBeenCalled();
90
+ expect(onDisabledClick).toHaveBeenCalledTimes(3);
91
+ });
92
+
93
+ it('a type="submit" button does not submit its form', async () => {
94
+ const user = userEvent.setup();
95
+ const onSubmit = vi.fn((event: React.FormEvent) => event.preventDefault());
96
+ wrap(
97
+ <form onSubmit={onSubmit}>
98
+ <input aria-label="이름" />
99
+ <Button type="submit" disabled focusableWhenDisabled>
100
+ 보내기
101
+ </Button>
102
+ </form>
103
+ );
104
+ await user.click(screen.getByRole('button', { name: '보내기' }));
105
+ // Implicit submission: Enter in a field clicks the default button.
106
+ await user.type(screen.getByRole('textbox', { name: '이름' }), '{Enter}');
107
+ expect(onSubmit).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('once enabled, behaves like any button', async () => {
111
+ const user = userEvent.setup();
112
+ const onClick = vi.fn();
113
+ const onDisabledClick = vi.fn();
114
+ wrap(
115
+ <Button
116
+ disabled={false}
117
+ focusableWhenDisabled
118
+ onClick={onClick}
119
+ onDisabledClick={onDisabledClick}
120
+ >
121
+ 결제
122
+ </Button>
123
+ );
124
+ const button = screen.getByRole('button', { name: '결제' });
125
+ expect(button).not.toHaveAttribute('aria-disabled');
126
+ expect(button).not.toHaveAttribute('data-disabled');
127
+ await user.click(button);
128
+ expect(onClick).toHaveBeenCalledTimes(1);
129
+ expect(onDisabledClick).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it('loading keeps the native disabled attribute', () => {
133
+ const onDisabledClick = vi.fn();
134
+ wrap(
135
+ <Button
136
+ disabled
137
+ loading
138
+ focusableWhenDisabled
139
+ onDisabledClick={onDisabledClick}
140
+ >
141
+ 결제
142
+ </Button>
143
+ );
144
+ const button = screen.getByRole('button');
145
+ expect(button).toBeDisabled();
146
+ expect(button).not.toHaveAttribute('aria-disabled');
147
+ expect(getComputedStyle(button).cursor).toBe('progress');
148
+ });
149
+
150
+ it('the click does not reach an ancestor', async () => {
151
+ const user = userEvent.setup();
152
+ const onRowClick = vi.fn();
153
+ const onDisabledClick = vi.fn();
154
+ wrap(
155
+ <div onClick={onRowClick}>
156
+ <Button
157
+ disabled
158
+ focusableWhenDisabled
159
+ onDisabledClick={onDisabledClick}
160
+ >
161
+ 결제
162
+ </Button>
163
+ </div>
164
+ );
165
+ await user.click(screen.getByRole('button', { name: '결제' }));
166
+ expect(onDisabledClick).toHaveBeenCalledTimes(1);
167
+ expect(onRowClick).not.toHaveBeenCalled();
168
+ });
169
+
170
+ it('a call-site aria-disabled / data-disabled cannot undo it', () => {
171
+ wrap(
172
+ <Button
173
+ disabled
174
+ focusableWhenDisabled
175
+ aria-disabled={false}
176
+ data-disabled={undefined}
177
+ >
178
+ 결제
179
+ </Button>
180
+ );
181
+ const button = screen.getByRole('button', { name: '결제' });
182
+ expect(button).toHaveAttribute('aria-disabled', 'true');
183
+ expect(button).toHaveAttribute('data-disabled');
184
+ });
185
+
186
+ describe('without the prop nothing changes', () => {
187
+ it('disabled is the native attribute, out of the Tab order', async () => {
188
+ const user = userEvent.setup();
189
+ const onClick = vi.fn();
190
+ const onDisabledClick = vi.fn();
191
+ wrap(
192
+ <>
193
+ <Button>before</Button>
194
+ <Button disabled onClick={onClick} onDisabledClick={onDisabledClick}>
195
+ 결제
196
+ </Button>
197
+ </>
198
+ );
199
+ const button = screen.getByRole('button', { name: '결제' });
200
+ expect(button).toBeDisabled();
201
+ expect(button).not.toHaveAttribute('aria-disabled');
202
+ await user.tab();
203
+ await user.tab();
204
+ expect(button).not.toHaveFocus();
205
+ await user.click(button);
206
+ expect(onClick).not.toHaveBeenCalled();
207
+ expect(onDisabledClick).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it('an enabled button calls onClick', async () => {
211
+ const user = userEvent.setup();
212
+ const onClick = vi.fn();
213
+ wrap(<Button onClick={onClick}>결제</Button>);
214
+ await user.click(screen.getByRole('button', { name: '결제' }));
215
+ expect(onClick).toHaveBeenCalledTimes(1);
216
+ });
217
+ });
218
+ });
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode, useId, useRef, useState } from 'react';
2
2
  import { Box, Flex, Grid, HStack, Text, VStack } from '@chakra-ui/react';
3
3
  import { Meta, StoryObj } from '@storybook/react';
4
4
 
@@ -264,6 +264,71 @@ export const AsLink: Story = {
264
264
  ),
265
265
  };
266
266
 
267
+ /**
268
+ * `focusableWhenDisabled` keeps a disabled button reachable so it can explain
269
+ * itself. Tab to 결제 with the box unticked, then press it: `onClick` does not
270
+ * run, `onDisabledClick` moves focus to the box and the hint turns red.
271
+ *
272
+ * It is disabled by `aria-disabled="true"` instead of the native attribute, so
273
+ * it looks the same as the native disabled button next to it, ignores hover
274
+ * and press, and does not submit a form.
275
+ */
276
+ export const FocusableWhenDisabled: Story = {
277
+ render: function Render() {
278
+ const [agreed, setAgreed] = useState(false);
279
+ const [nudged, setNudged] = useState(false);
280
+ const [paid, setPaid] = useState(0);
281
+ const checkboxRef = useRef<HTMLInputElement>(null);
282
+ const hintId = useId();
283
+ return (
284
+ <VStack align="flex-start" gap={3} p={4} maxW="360px">
285
+ <label style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
286
+ <input
287
+ ref={checkboxRef}
288
+ type="checkbox"
289
+ checked={agreed}
290
+ onChange={(event) => {
291
+ setAgreed(event.target.checked);
292
+ setNudged(false);
293
+ }}
294
+ />
295
+ <Text fontSize="sm">이용 및 환불 안내에 동의합니다</Text>
296
+ </label>
297
+ {!agreed && (
298
+ <Text
299
+ id={hintId}
300
+ fontSize="sm"
301
+ color={nudged ? 'danger.fg' : 'fg.muted'}
302
+ >
303
+ 안내에 동의하면 결제할 수 있습니다.
304
+ </Text>
305
+ )}
306
+ <HStack gap={3}>
307
+ <Button
308
+ variant="primary"
309
+ disabled={!agreed}
310
+ focusableWhenDisabled
311
+ aria-describedby={agreed ? undefined : hintId}
312
+ onDisabledClick={() => {
313
+ setNudged(true);
314
+ checkboxRef.current?.focus();
315
+ }}
316
+ onClick={() => setPaid((n) => n + 1)}
317
+ >
318
+ 결제
319
+ </Button>
320
+ <Button variant="primary" disabled>
321
+ native disabled
322
+ </Button>
323
+ </HStack>
324
+ <Text fontSize="sm" color="fg.muted">
325
+ onClick: {paid}
326
+ </Text>
327
+ </VStack>
328
+ );
329
+ },
330
+ };
331
+
267
332
  // ───────────────────────── Usage-page specimens ─────────────────────────
268
333
  // Pulled into `Button.usage.mdx` with <Canvas of={…} />. Hidden from the
269
334
  // sidebar (`!dev`); being stories, they type-check with the rest of the code.
@@ -1,4 +1,4 @@
1
- import { forwardRef } from 'react';
1
+ import { forwardRef, MouseEvent } from 'react';
2
2
  import { Button as ChakraButton, SystemStyleObject } from '@chakra-ui/react';
3
3
 
4
4
  import { useTranslate } from '@/hooks/useTranslate';
@@ -38,6 +38,10 @@ const ButtonImpl = forwardRef<HTMLButtonElement, ButtonProps>(
38
38
  isDestructive: isDestructiveProp = false,
39
39
  size,
40
40
  lift,
41
+ disabled,
42
+ focusableWhenDisabled = false,
43
+ onDisabledClick,
44
+ onClick,
41
45
  children,
42
46
  _hover: hoverProp,
43
47
  _active: activeProp,
@@ -79,6 +83,24 @@ const ButtonImpl = forwardRef<HTMLButtonElement, ButtonProps>(
79
83
 
80
84
  const base = getButtonStyles(variant, isDestructive);
81
85
 
86
+ // `focusableWhenDisabled`: disabled by ARIA, not by the attribute, so the
87
+ // button stays focusable and can answer a press. `aria-disabled=true` is
88
+ // in Chakra's `_disabled` condition, so the disabled colours and cursor
89
+ // still apply; `data-disabled` is what `_hover` / `_active` exclude, so
90
+ // without it the button would still react to the pointer. `loading` keeps
91
+ // the native attribute (Chakra sets it) — see `focusableWhenDisabled`.
92
+ const softDisabled = !!disabled && focusableWhenDisabled && !rest.loading;
93
+ const handleClick = softDisabled
94
+ ? (event: MouseEvent<HTMLButtonElement>) => {
95
+ // Also what stops a `type="submit"` button from submitting.
96
+ event.preventDefault();
97
+ // A native disabled button sends no click to its ancestors either —
98
+ // a clickable row around it must not run its own action.
99
+ event.stopPropagation();
100
+ onDisabledClick?.(event);
101
+ }
102
+ : onClick;
103
+
82
104
  // Merged rather than spread after: each variant already owns `_hover` and
83
105
  // `_active` for its colours, and replacing either would drop them.
84
106
  const styles = lift
@@ -162,7 +184,15 @@ const ButtonImpl = forwardRef<HTMLButtonElement, ButtonProps>(
162
184
  data-variant={variant}
163
185
  data-destructive={isDestructive || undefined}
164
186
  loadingText={loadingText}
187
+ disabled={softDisabled ? undefined : disabled}
188
+ onClick={handleClick}
165
189
  {...rest}
190
+ // After `rest`, so a stray `aria-disabled` / `data-disabled` from the
191
+ // call site or a merging `asChild` parent cannot undo it.
192
+ {...(softDisabled && {
193
+ 'aria-disabled': true,
194
+ 'data-disabled': '',
195
+ })}
166
196
  // The `action` icon rule lives in `css` (a nested selector cannot be a
167
197
  // style prop); merged so a call site's `css` adds to it.
168
198
  css={mergeCss(base.css as SystemStyleObject | undefined, cssProp)}
@@ -1,4 +1,4 @@
1
- import type { ElementType } from 'react';
1
+ import type { ElementType, MouseEvent } from 'react';
2
2
  import { ButtonProps as ChakraButtonProps } from '@chakra-ui/react';
3
3
 
4
4
  // Relative, NOT `@/types/polymorphic`. Path aliases survive into the emitted
@@ -107,6 +107,43 @@ export type ButtonOwnProps = Omit<
107
107
  * @default false
108
108
  */
109
109
  lift?: boolean;
110
+ /**
111
+ * Keeps a `disabled` button in the Tab order and lets it receive clicks, so
112
+ * the screen can say *why* it is disabled.
113
+ *
114
+ * A natively `disabled` `<button>` cannot be focused and swallows every
115
+ * click: a keyboard or screen reader user may never find it, and a mouse
116
+ * user who presses it gets no answer at all. With this prop, `disabled`
117
+ * renders as `aria-disabled="true"` (plus `data-disabled`) instead of the
118
+ * native attribute:
119
+ *
120
+ * - the disabled colours, the `not-allowed` cursor and the "no hover, no
121
+ * press" behaviour are the same as a native disabled button;
122
+ * - Tab still reaches it and the focus ring shows;
123
+ * - activating it (click, Enter, Space) does **not** call `onClick`, and its
124
+ * default action is cancelled, so a `type="submit"` button does not submit
125
+ * its form; the click does not bubble to ancestors; `onDisabledClick` is
126
+ * called instead.
127
+ *
128
+ * Only activation is blocked: other handlers (`onKeyDown`, `onPointerDown`,
129
+ * …) still run, as they would on any focusable element. Inside a trigger
130
+ * (`Menu.Trigger asChild`, `Popover.Trigger asChild`) disable the root as
131
+ * well. Meant for `<button>`: with `as="a"` the `href` stays, so middle-click
132
+ * still opens it.
133
+ *
134
+ * Has no effect unless `disabled` is set. `loading` keeps the native
135
+ * `disabled` — a busy button is working, not refusing.
136
+ *
137
+ * @default false
138
+ */
139
+ focusableWhenDisabled?: boolean;
140
+ /**
141
+ * Called when a `disabled` + `focusableWhenDisabled` button is activated
142
+ * (click, Enter, Space) — where `onClick` would have been. Use it to explain
143
+ * the reason or move focus to what is missing (an unticked consent box).
144
+ * Never called on a natively disabled button, and never while `loading`.
145
+ */
146
+ onDisabledClick?: (event: MouseEvent<HTMLButtonElement>) => void;
110
147
  };
111
148
 
112
149
  /**
@@ -69,7 +69,7 @@ A control that does something on this screen: save, cancel, run, delete. One
69
69
 
70
70
  </Ko>
71
71
 
72
- <ArgTypes of={ButtonStories} include={['variant', 'isDestructive', 'lift']} />
72
+ <ArgTypes of={ButtonStories} include={['variant', 'isDestructive', 'lift', 'focusableWhenDisabled', 'onDisabledClick']} />
73
73
 
74
74
  <En>
75
75
 
@@ -203,6 +203,13 @@ other actions, and keep the red fill for the final confirmation.
203
203
  ko="loadingText 없이 loading만 주지 마세요. 라벨이 스피너 뒤로 숨어 눈에 보이는 글자가 없어지고, 화면 낭독기에도 이름이 전달되지 않을 수 있습니다."
204
204
  />
205
205
 
206
+ <Canvas of={ButtonStories.FocusableWhenDisabled} />
207
+ <Verdict
208
+ kind="do"
209
+ en="When the reason a button is disabled can be out of view, add focusableWhenDisabled and answer onDisabledClick by moving focus to what is missing. Without it the button cannot be reached by Tab and a press does nothing."
210
+ ko="버튼이 비활성인 이유가 화면 밖에 있을 수 있다면 focusableWhenDisabled를 붙이고, onDisabledClick에서 빠진 항목으로 초점을 옮깁니다. 붙이지 않으면 Tab으로 닿지 않고, 눌러도 아무 반응이 없습니다."
211
+ />
212
+
206
213
  <En>
207
214
 
208
215
  ## Content
@@ -244,7 +251,11 @@ What you still have to do:
244
251
  the spinner.
245
252
  - **Say why it is disabled.** A disabled `<button>` cannot be focused, so a
246
253
  keyboard or screen reader user may never find it. Put the reason in text
247
- near it, or keep the button enabled and explain on click.
254
+ near it. When the reason is out of view (a consent box at the bottom of a
255
+ long form), add `focusableWhenDisabled`: the button stays in the Tab order
256
+ as `aria-disabled="true"`, looks and refuses the same, and calls
257
+ `onDisabledClick` instead of `onClick` when pressed — use it to move focus
258
+ to what is missing. Point `aria-describedby` at the reason text.
248
259
  - **Only phrasing content inside** (ACCESSIBILITY.md §5). No `<div>` or `<p>`
249
260
  inside a Button; wrap with `as="span"` and set `display`.
250
261
  - **No button inside another control.** Don't put a Button inside a clickable
@@ -300,7 +311,10 @@ Button이 알아서 하는 일
300
311
  - **`loading`에는 `loadingText`를 함께 줍니다.** 없으면 라벨이 스피너 뒤로 숨습니다.
301
312
  - **왜 비활성인지 알립니다.** 비활성 `<button>`은 초점을 받지 못해 키보드나 화면
302
313
  낭독기 사용자는 버튼이 있다는 것조차 모를 수 있습니다. 가까운 곳에 이유를 글로
303
- 적거나, 버튼을 활성으로 두고 눌렀을 때 설명합니다.
314
+ 적습니다. 이유가 화면 밖에 있다면(긴 폼 맨 아래의 동의 체크박스) `focusableWhenDisabled`를
315
+ 붙입니다. 버튼이 `aria-disabled="true"`로 Tab 순서에 남고, 모양과 막히는 동작은
316
+ 같으며, 누르면 `onClick` 대신 `onDisabledClick`을 부릅니다. 여기서 빠진 항목으로
317
+ 초점을 옮기고, `aria-describedby`로 이유 문구를 가리킵니다.
304
318
  - **안에는 구문 콘텐츠만 넣습니다**(ACCESSIBILITY.md 5절). Button 안에 `<div>`나
305
319
  `<p>`를 넣지 않습니다. 감싸야 한다면 `as="span"`에 `display`를 지정합니다.
306
320
  - **컨트롤 안에 버튼을 넣지 않습니다.** 누를 수 있는 카드나 `role="button"` 래퍼 안에