@mindlogic-ai/logician-ui 4.0.0-alpha.21 → 4.0.0-alpha.22
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/dist/components/ErrorFallback/ErrorFallback.d.ts.map +1 -1
- package/dist/components/ErrorFallback/ErrorFallback.js +13 -2
- package/dist/components/ErrorFallback/ErrorFallback.js.map +1 -1
- package/dist/components/ErrorFallback/ErrorFallback.mjs +13 -2
- package/dist/components/ErrorFallback/ErrorFallback.mjs.map +1 -1
- package/dist/components/FileInput/FileInput.d.ts.map +1 -1
- package/dist/components/FileInput/FileInput.js +7 -1
- package/dist/components/FileInput/FileInput.js.map +1 -1
- package/dist/components/FileInput/FileInput.mjs +7 -1
- package/dist/components/FileInput/FileInput.mjs.map +1 -1
- package/dist/components/Markdown/Markdown.d.ts.map +1 -1
- package/dist/components/Markdown/Markdown.js +6 -1
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Markdown/Markdown.mjs +6 -1
- package/dist/components/Markdown/Markdown.mjs.map +1 -1
- package/dist/components/Pagination/Pagination.d.ts.map +1 -1
- package/dist/components/Pagination/Pagination.js +6 -1
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Pagination/Pagination.mjs +6 -1
- package/dist/components/Pagination/Pagination.mjs.map +1 -1
- package/dist/components/Radio/RadioControl.d.ts.map +1 -1
- package/dist/components/Radio/RadioControl.js +14 -1
- package/dist/components/Radio/RadioControl.js.map +1 -1
- package/dist/components/Radio/RadioControl.mjs +15 -2
- package/dist/components/Radio/RadioControl.mjs.map +1 -1
- package/dist/components/Select/ComboboxField.d.ts +1 -1
- package/dist/components/Select/ComboboxField.d.ts.map +1 -1
- package/dist/components/Select/ComboboxField.js +2 -2
- package/dist/components/Select/ComboboxField.js.map +1 -1
- package/dist/components/Select/ComboboxField.mjs +2 -2
- package/dist/components/Select/ComboboxField.mjs.map +1 -1
- package/dist/components/Select/Select.types.d.ts +16 -0
- package/dist/components/Select/Select.types.d.ts.map +1 -1
- package/dist/components/Select/SelectField.d.ts +1 -1
- package/dist/components/Select/SelectField.d.ts.map +1 -1
- package/dist/components/Select/SelectField.js +2 -2
- package/dist/components/Select/SelectField.js.map +1 -1
- package/dist/components/Select/SelectField.mjs +2 -2
- package/dist/components/Select/SelectField.mjs.map +1 -1
- package/dist/components/Slider/Slider.d.ts.map +1 -1
- package/dist/components/Slider/Slider.js +2 -2
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Slider/Slider.mjs +3 -3
- package/dist/components/Slider/Slider.mjs.map +1 -1
- package/dist/components/Slider/Slider.types.d.ts +15 -0
- package/dist/components/Slider/Slider.types.d.ts.map +1 -1
- package/dist/components/Typography/Link.d.ts.map +1 -1
- package/dist/components/Typography/Link.js +17 -7
- package/dist/components/Typography/Link.js.map +1 -1
- package/dist/components/Typography/Link.mjs +17 -7
- package/dist/components/Typography/Link.mjs.map +1 -1
- package/dist/components/Typography/Subtitle.d.ts +17 -0
- package/dist/components/Typography/Subtitle.d.ts.map +1 -1
- package/dist/components/Typography/Subtitle.js +18 -1
- package/dist/components/Typography/Subtitle.js.map +1 -1
- package/dist/components/Typography/Subtitle.mjs +18 -1
- package/dist/components/Typography/Subtitle.mjs.map +1 -1
- package/dist/components/Workflow/canvas/NodePalette/NodePalette.d.ts.map +1 -1
- package/dist/components/Workflow/canvas/NodePalette/NodePalette.js +19 -2
- package/dist/components/Workflow/canvas/NodePalette/NodePalette.js.map +1 -1
- package/dist/components/Workflow/canvas/NodePalette/NodePalette.mjs +19 -2
- package/dist/components/Workflow/canvas/NodePalette/NodePalette.mjs.map +1 -1
- package/package.json +2 -1
- package/src/components/ErrorFallback/ErrorFallback.tsx +30 -3
- package/src/components/FileInput/FileInput.tsx +11 -2
- package/src/components/Markdown/Markdown.tsx +6 -1
- package/src/components/Pagination/Pagination.tsx +5 -0
- package/src/components/Radio/RadioControl.tsx +20 -6
- package/src/components/Select/ComboboxField.tsx +11 -1
- package/src/components/Select/Select.types.ts +16 -0
- package/src/components/Select/SelectField.tsx +14 -1
- package/src/components/Slider/Slider.tsx +16 -1
- package/src/components/Slider/Slider.types.ts +15 -0
- package/src/components/Typography/Link.tsx +17 -7
- package/src/components/Typography/Subtitle.tsx +17 -1
- package/src/components/Workflow/canvas/NodePalette/NodePalette.tsx +12 -2
- package/src/components/a11y.kwcag.test.tsx +295 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
1
2
|
import { Box, Flex, VStack } from '@chakra-ui/react';
|
|
2
3
|
|
|
3
4
|
import { useTranslate } from '@/hooks/useTranslate';
|
|
@@ -20,6 +21,8 @@ export function ErrorFallback({
|
|
|
20
21
|
}: ErrorFallbackProps) {
|
|
21
22
|
const translate = useTranslate();
|
|
22
23
|
const showToast = useToast();
|
|
24
|
+
// Names the landmark below with the same words the card shows as its title.
|
|
25
|
+
const titleId = useId();
|
|
23
26
|
|
|
24
27
|
const errorInfo = {
|
|
25
28
|
errorId:
|
|
@@ -61,7 +64,22 @@ User Agent: ${errorInfo.userAgent}`;
|
|
|
61
64
|
};
|
|
62
65
|
|
|
63
66
|
return (
|
|
64
|
-
|
|
67
|
+
// A landmark, and a named one. This card replaces whatever failed — often
|
|
68
|
+
// the whole page, so the host's `<main>` goes with it — and a screen-reader
|
|
69
|
+
// user then has nothing to navigate by: axe reports every node under
|
|
70
|
+
// `region`, and there is no landmark to jump to (KWCAG 2.1 5.2.4.1 반복
|
|
71
|
+
// 영역 건너뛰기). A `<section>` labelled by the error title is a region
|
|
72
|
+
// landmark wherever it renders, which `<main>` would not be — a segment-level
|
|
73
|
+
// boundary leaves the host's `<main>` in place, and two of those is its own
|
|
74
|
+
// failure.
|
|
75
|
+
<Flex
|
|
76
|
+
as="section"
|
|
77
|
+
aria-labelledby={titleId}
|
|
78
|
+
minH="100vh"
|
|
79
|
+
align="center"
|
|
80
|
+
justify="center"
|
|
81
|
+
p={8}
|
|
82
|
+
>
|
|
65
83
|
<Container disableResponsive maxW="container.sm">
|
|
66
84
|
<Card
|
|
67
85
|
p={12}
|
|
@@ -83,7 +101,12 @@ User Agent: ${errorInfo.userAgent}`;
|
|
|
83
101
|
|
|
84
102
|
{/* Error Title */}
|
|
85
103
|
<VStack gap={3} align="center" w="full">
|
|
86
|
-
<H1
|
|
104
|
+
<H1
|
|
105
|
+
id={titleId}
|
|
106
|
+
color="fg.default"
|
|
107
|
+
textStyle="h2"
|
|
108
|
+
fontWeight="bold"
|
|
109
|
+
>
|
|
87
110
|
{translate('error_boundary_title')}
|
|
88
111
|
</H1>
|
|
89
112
|
<Subtitle color="fg.muted" textStyle="h5" maxW="md">
|
|
@@ -115,7 +138,11 @@ User Agent: ${errorInfo.userAgent}`;
|
|
|
115
138
|
p={6}
|
|
116
139
|
w="full"
|
|
117
140
|
>
|
|
118
|
-
|
|
141
|
+
{/* `as="h2"`: `H4` is the type size wanted here, but this block
|
|
142
|
+
sits directly under the card's `<h1>` — an `<h4>` there skips
|
|
143
|
+
two levels and tells a screen-reader user there are sections
|
|
144
|
+
they missed (KWCAG 5.2.4.2 제목 제공). */}
|
|
145
|
+
<H4 as="h2" color="primary.dark" mb={4} textAlign="center">
|
|
119
146
|
{translate('support_information')}
|
|
120
147
|
</H4>
|
|
121
148
|
<VStack gap={2} align="stretch" mb={4}>
|
|
@@ -27,8 +27,12 @@ export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
|
+
// No `role="button"` on this wrapper. It is not a button — the `<label>`
|
|
31
|
+
// inside is what activates the file input — and claiming the role put an
|
|
32
|
+
// interactive element inside another (axe `nested-interactive`), which is
|
|
33
|
+
// a state screen readers have no sensible way to announce
|
|
34
|
+
// (KWCAG 2.1 5.4.2.1 웹 애플리케이션 접근성 준수).
|
|
30
35
|
<Flex
|
|
31
|
-
role="button"
|
|
32
36
|
w="100%"
|
|
33
37
|
h="100%"
|
|
34
38
|
maxH={32}
|
|
@@ -42,8 +46,13 @@ export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
|
|
|
42
46
|
borderRadius="md"
|
|
43
47
|
{...containerStyle}
|
|
44
48
|
>
|
|
49
|
+
{/* `className="group"`, not `role="group"`. The role was doing nothing for
|
|
50
|
+
assistive tech (a `<label>` may not be a group — axe
|
|
51
|
+
`aria-allowed-role`) and nothing for the hover effect either: Chakra's
|
|
52
|
+
`_groupHover` compiles to `.group:hover &`, so it needs the CLASS.
|
|
53
|
+
The reveal-on-hover below has therefore never fired. */}
|
|
45
54
|
<label
|
|
46
|
-
|
|
55
|
+
className="group"
|
|
47
56
|
style={{ width: '100%', height: '100%', cursor: 'pointer' }}
|
|
48
57
|
>
|
|
49
58
|
{bgImage && (
|
|
@@ -22,7 +22,12 @@ const H2Wrapper = (props: any) => <H2 {...props} />;
|
|
|
22
22
|
const H3Wrapper = (props: any) => <H3 {...props} />;
|
|
23
23
|
const H4Wrapper = (props: any) => <H4 {...props} />;
|
|
24
24
|
const H5Wrapper = (props: any) => <H5 {...props} />;
|
|
25
|
-
|
|
25
|
+
// `as="h6"` explicitly. `Subtitle` renders a paragraph by default now (its call
|
|
26
|
+
// sites are overwhelmingly helper copy that was never a heading), but this
|
|
27
|
+
// wrapper is markdown's `h6` — a heading the author actually wrote. Inheriting
|
|
28
|
+
// the new default here would silently drop `###### 제목` out of the document
|
|
29
|
+
// outline, which is the same KWCAG 5.2.4.2 (제목 제공) defect inverted.
|
|
30
|
+
const SubtitleWrapper = (props: any) => <Subtitle as="h6" {...props} />;
|
|
26
31
|
const TheadWrapper = (props: any) => <Thead {...props} />;
|
|
27
32
|
const TbodyWrapper = (props: any) => <Tbody {...props} />;
|
|
28
33
|
const TrWrapper = (props: any) => <Tr {...props} />;
|
|
@@ -81,6 +81,11 @@ export const Pagination = ({
|
|
|
81
81
|
<SelectField<number>
|
|
82
82
|
size="sm"
|
|
83
83
|
width="5.5rem"
|
|
84
|
+
// The visible label for this control is the "개씩 보기" text beside
|
|
85
|
+
// it, which the select itself cannot claim — so it carries the
|
|
86
|
+
// same words as its own hidden label rather than shipping
|
|
87
|
+
// nameless (KWCAG 5.3.4.1 레이블 제공).
|
|
88
|
+
ariaLabel={translate('pagination_items_per_page') as string}
|
|
84
89
|
options={itemsPerPageOptions.map((option) => ({
|
|
85
90
|
label: option.toString(),
|
|
86
91
|
value: option,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef } from 'react';
|
|
1
|
+
import { forwardRef, useId } from 'react';
|
|
2
2
|
import { RadioGroup, RadioGroupItemControlProps } from '@chakra-ui/react';
|
|
3
3
|
|
|
4
4
|
import { focusRing } from '@/utils/focusRing';
|
|
@@ -6,9 +6,23 @@ import { focusRing } from '@/utils/focusRing';
|
|
|
6
6
|
export const RadioControl = forwardRef<
|
|
7
7
|
HTMLDivElement,
|
|
8
8
|
RadioGroupItemControlProps
|
|
9
|
-
>((props, ref) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
>((props, ref) => {
|
|
10
|
+
// Chakra derives the indicator's id from the same `ids.itemControl` it gives
|
|
11
|
+
// the control it sits inside, so every radio on the page shipped TWO elements
|
|
12
|
+
// with one id — a markup error under KWCAG 2.1 5.4.1.1 마크업 오류 방지, which
|
|
13
|
+
// Korean evaluators still count even though WCAG 2.2 dropped SC 4.1.1. It also
|
|
14
|
+
// breaks anything that resolves the control by id, since `getElementById`
|
|
15
|
+
// returns whichever came first.
|
|
16
|
+
//
|
|
17
|
+
// Nothing references the indicator — it is the dot inside the control — so any
|
|
18
|
+
// unique value fixes it, and `useId` is the one that stays stable across
|
|
19
|
+
// server and client render.
|
|
20
|
+
const indicatorId = useId();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<RadioGroup.ItemControl ref={ref} {...focusRing} {...props}>
|
|
24
|
+
<RadioGroup.ItemIndicator id={indicatorId} />
|
|
25
|
+
</RadioGroup.ItemControl>
|
|
26
|
+
);
|
|
27
|
+
});
|
|
14
28
|
RadioControl.displayName = 'Radio.Control';
|
|
@@ -19,6 +19,7 @@ export function ComboboxField<T = string>({
|
|
|
19
19
|
onChange,
|
|
20
20
|
placeholder,
|
|
21
21
|
label,
|
|
22
|
+
ariaLabel,
|
|
22
23
|
size = 'md',
|
|
23
24
|
invalid,
|
|
24
25
|
disabled,
|
|
@@ -60,7 +61,16 @@ export function ComboboxField<T = string>({
|
|
|
60
61
|
}}
|
|
61
62
|
onInputValueChange={(details) => filter(details.inputValue)}
|
|
62
63
|
>
|
|
63
|
-
{label
|
|
64
|
+
{/* Same as `SelectField`: the label part is the target of the input's
|
|
65
|
+
`aria-labelledby`, so omitting it leaves the control unnamed rather
|
|
66
|
+
than unlabelled (KWCAG 5.3.4.1 레이블 제공). */}
|
|
67
|
+
{label != null ? (
|
|
68
|
+
<Combobox.Label>{label}</Combobox.Label>
|
|
69
|
+
) : (
|
|
70
|
+
(ariaLabel ?? placeholder) && (
|
|
71
|
+
<Combobox.Label srOnly>{ariaLabel ?? placeholder}</Combobox.Label>
|
|
72
|
+
)
|
|
73
|
+
)}
|
|
64
74
|
<Combobox.Control>
|
|
65
75
|
<Combobox.Input placeholder={placeholder} />
|
|
66
76
|
<Combobox.IndicatorGroup>
|
|
@@ -20,6 +20,22 @@ interface FieldBaseProps<T> {
|
|
|
20
20
|
placeholder?: string;
|
|
21
21
|
/** Optional label rendered above the control. */
|
|
22
22
|
label?: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Accessible name for the trigger when no visible `label` is rendered — a
|
|
25
|
+
* field labelled by a table column header, a `FieldRow`, or the sentence
|
|
26
|
+
* around it.
|
|
27
|
+
*
|
|
28
|
+
* Without one of `label` / `ariaLabel` / `placeholder` the trigger has **no
|
|
29
|
+
* accessible name at all**: the composition points `aria-labelledby` at a
|
|
30
|
+
* label part that is not rendered, the reference dangles, and the control
|
|
31
|
+
* announces as a bare "button" however much text it shows (KWCAG 2.1
|
|
32
|
+
* 5.3.4.1 레이블 제공, axe `button-name`).
|
|
33
|
+
*
|
|
34
|
+
* Rendered as a visually hidden label rather than an `aria-label` so the name
|
|
35
|
+
* is in the server-rendered markup — an attribute applied after hydration
|
|
36
|
+
* leaves a window in which the control is nameless.
|
|
37
|
+
*/
|
|
38
|
+
ariaLabel?: string;
|
|
23
39
|
size?: SelectSize;
|
|
24
40
|
invalid?: boolean;
|
|
25
41
|
disabled?: boolean;
|
|
@@ -19,6 +19,7 @@ export function SelectField<T = string>({
|
|
|
19
19
|
onChange,
|
|
20
20
|
placeholder,
|
|
21
21
|
label,
|
|
22
|
+
ariaLabel,
|
|
22
23
|
size = 'md',
|
|
23
24
|
invalid,
|
|
24
25
|
disabled,
|
|
@@ -61,7 +62,19 @@ export function SelectField<T = string>({
|
|
|
61
62
|
}}
|
|
62
63
|
>
|
|
63
64
|
<Select.HiddenSelect />
|
|
64
|
-
{label
|
|
65
|
+
{/* The label part is what the trigger's `aria-labelledby` points at, and
|
|
66
|
+
Chakra emits that reference whether or not the part is rendered. Leave
|
|
67
|
+
it out and the reference dangles, so the trigger resolves to no name —
|
|
68
|
+
a "button" to a screen reader, whatever the selected option says
|
|
69
|
+
(KWCAG 5.3.4.1 레이블 제공). So it is always rendered: visibly when the
|
|
70
|
+
field has a label, hidden when the name lives elsewhere on screen. */}
|
|
71
|
+
{label != null ? (
|
|
72
|
+
<Select.Label>{label}</Select.Label>
|
|
73
|
+
) : (
|
|
74
|
+
(ariaLabel ?? placeholder) && (
|
|
75
|
+
<Select.Label srOnly>{ariaLabel ?? placeholder}</Select.Label>
|
|
76
|
+
)
|
|
77
|
+
)}
|
|
65
78
|
<Select.Control>
|
|
66
79
|
<Select.Trigger>
|
|
67
80
|
<Select.ValueText placeholder={placeholder} />
|
|
@@ -15,7 +15,14 @@ import { SliderControl } from './SliderControl';
|
|
|
15
15
|
*/
|
|
16
16
|
export const Slider = forwardRef(
|
|
17
17
|
(
|
|
18
|
-
{
|
|
18
|
+
{
|
|
19
|
+
value,
|
|
20
|
+
defaultValue,
|
|
21
|
+
onValueChange,
|
|
22
|
+
ariaLabel,
|
|
23
|
+
children,
|
|
24
|
+
...rest
|
|
25
|
+
}: SliderProps,
|
|
19
26
|
ref?: ForwardedRef<HTMLDivElement>
|
|
20
27
|
) => {
|
|
21
28
|
// Check if children already contains SliderControl
|
|
@@ -42,6 +49,14 @@ export const Slider = forwardRef(
|
|
|
42
49
|
onValueChange={onValueChange}
|
|
43
50
|
{...rest}
|
|
44
51
|
>
|
|
52
|
+
{/* The label part is what each thumb's `aria-labelledby` points at, and
|
|
53
|
+
the reference is emitted whether or not the part is rendered — so a
|
|
54
|
+
slider composed without one is not merely unlabelled, it is unnamed
|
|
55
|
+
(KWCAG 5.3.4.1 레이블 제공). Hidden, because these call sites carry
|
|
56
|
+
their label as an adjacent icon or time display. */}
|
|
57
|
+
{ariaLabel && (
|
|
58
|
+
<ChakraSlider.Label srOnly>{ariaLabel}</ChakraSlider.Label>
|
|
59
|
+
)}
|
|
45
60
|
{wrappedChildren}
|
|
46
61
|
</ChakraSlider.Root>
|
|
47
62
|
);
|
|
@@ -11,5 +11,20 @@ export interface SliderProps extends Omit<
|
|
|
11
11
|
defaultValue?: number[];
|
|
12
12
|
/** Callback when slider value changes */
|
|
13
13
|
onValueChange?: Slider.RootProps['onValueChange'];
|
|
14
|
+
/**
|
|
15
|
+
* Accessible name for the slider, when no visible `Slider.Label` is composed
|
|
16
|
+
* in — a seek bar next to a time display, a volume control beside a mute
|
|
17
|
+
* button.
|
|
18
|
+
*
|
|
19
|
+
* Without it the thumb has **no accessible name**: Zag points each thumb's
|
|
20
|
+
* `aria-labelledby` at a label part, and a composition that does not render
|
|
21
|
+
* one leaves that reference dangling — the control announces as a bare
|
|
22
|
+
* "slider" (KWCAG 2.1 5.3.4.1 레이블 제공, axe `aria-input-field-name`).
|
|
23
|
+
*
|
|
24
|
+
* Rendered as a visually hidden label rather than an `aria-label` array so it
|
|
25
|
+
* works the same whether the slider has one thumb or several, and so the name
|
|
26
|
+
* is in the server-rendered markup.
|
|
27
|
+
*/
|
|
28
|
+
ariaLabel?: string;
|
|
14
29
|
children?: ReactNode;
|
|
15
30
|
}
|
|
@@ -7,15 +7,25 @@ export interface LinkCustomProps extends Omit<LinkProps, 'variant'> {
|
|
|
7
7
|
|
|
8
8
|
export const Link = forwardRef<HTMLAnchorElement, LinkCustomProps>(
|
|
9
9
|
({ color, variant, ...rest }, ref) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// Theme-aware, because `.main` is tuned against a WHITE page. On the dark
|
|
11
|
+
// canvas (#181A20) `primary.main` measures 4.19:1 and `danger.main` 4.26:1
|
|
12
|
+
// — both under the 4.5:1 that KWCAG 2.1 5.1.3.3 (명도 대비) asks of body
|
|
13
|
+
// text, and every link in a consuming app inherits it. The `.dark` end of
|
|
14
|
+
// each ramp is the far side from the PAGE rather than a fixed lightness, so
|
|
15
|
+
// in dark mode it resolves lighter: 6.68:1 and 6.38:1.
|
|
16
|
+
const defaultColor = { base: 'primary.main', _dark: 'primary.dark' };
|
|
17
|
+
const errorColor = { base: 'danger.main', _dark: 'danger.dark' };
|
|
18
|
+
const defaultHoverColor = { base: 'primary.dark', _dark: 'primary.main' };
|
|
19
|
+
const errorHoverColor = { base: 'danger.dark', _dark: 'danger.main' };
|
|
14
20
|
|
|
21
|
+
// `color ?? defaultColor`, not a `typeof color === 'string'` narrowing. The
|
|
22
|
+
// old form silently DISCARDED any non-string value, so a call site passing
|
|
23
|
+
// a per-theme `{ base, _dark }` — the documented Chakra way to fix exactly
|
|
24
|
+
// the contrast problem above — got the default back and no error, in the
|
|
25
|
+
// types or at runtime. A prop the type accepts and the component throws
|
|
26
|
+
// away is worse than one it rejects.
|
|
15
27
|
const linkColor =
|
|
16
|
-
variant === 'error'
|
|
17
|
-
? errorColor
|
|
18
|
-
: (typeof color === 'string' ? color : undefined) || defaultColor;
|
|
28
|
+
variant === 'error' ? errorColor : (color ?? defaultColor);
|
|
19
29
|
|
|
20
30
|
const hoverColor =
|
|
21
31
|
variant === 'error' ? errorHoverColor : defaultHoverColor;
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { forwardRef } from 'react';
|
|
2
2
|
import { Text, TextProps } from '@chakra-ui/react';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Supporting text under a heading — a subtitle by role in the page, not by
|
|
6
|
+
* document structure.
|
|
7
|
+
*
|
|
8
|
+
* Renders a paragraph. It used to render `<h6>`, which made every call site a
|
|
9
|
+
* level-6 heading in the document outline: a sentence of helper copy under an
|
|
10
|
+
* `<h1>` announced as a heading to a screen reader, and a jump from level 1 to
|
|
11
|
+
* level 6 in the heading list (KWCAG 2.1 5.2.4.2 제목 제공 / axe `heading-order`).
|
|
12
|
+
* A component chosen for its type scale should not decide the outline.
|
|
13
|
+
*
|
|
14
|
+
* A subtitle that genuinely IS the next heading level still says so at the call
|
|
15
|
+
* site, which is where that is known:
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <Subtitle as="h2">{sectionTitle}</Subtitle>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
4
21
|
export const Subtitle = forwardRef<HTMLParagraphElement, TextProps>(
|
|
5
22
|
(props, ref) => {
|
|
6
23
|
const { fontSize, ...rest } = props;
|
|
@@ -8,7 +25,6 @@ export const Subtitle = forwardRef<HTMLParagraphElement, TextProps>(
|
|
|
8
25
|
return (
|
|
9
26
|
<Text
|
|
10
27
|
ref={ref}
|
|
11
|
-
as="h6"
|
|
12
28
|
textStyle={fontSize ? undefined : 'subtitle'}
|
|
13
29
|
fontSize={fontSize}
|
|
14
30
|
color="fg.muted"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { DragEvent } from 'react';
|
|
3
|
+
import { DragEvent, useId } from 'react';
|
|
4
4
|
import { Box, HStack, Stack } from '@chakra-ui/react';
|
|
5
5
|
|
|
6
6
|
import { Card } from '@/components/Card';
|
|
@@ -95,6 +95,8 @@ function PaletteItem({ def }: { def: NodeTypeDef }) {
|
|
|
95
95
|
export function NodePalette({ onClose }: { onClose: () => void }) {
|
|
96
96
|
const { nodeTypes, readOnly } = useWorkflow();
|
|
97
97
|
const translate = useWorkflowTranslate();
|
|
98
|
+
// Names the region below with the same words the header shows.
|
|
99
|
+
const titleId = useId();
|
|
98
100
|
if (readOnly) return null;
|
|
99
101
|
|
|
100
102
|
// Group by category, preserving registration order within each group.
|
|
@@ -115,6 +117,14 @@ export function NodePalette({ onClose }: { onClose: () => void }) {
|
|
|
115
117
|
|
|
116
118
|
return (
|
|
117
119
|
<FloatingCard
|
|
120
|
+
// A named region, so the palette is something a screen-reader user can
|
|
121
|
+
// navigate TO. Its title used to be a `Subtitle`, which rendered `<h6>`
|
|
122
|
+
// and put a level-6 heading under the host page's `<h1>` (axe
|
|
123
|
+
// `heading-order`); `Subtitle` is a paragraph now, and a labelled region
|
|
124
|
+
// is what this panel actually is — a landmark, not an outline entry
|
|
125
|
+
// (KWCAG 2.1 5.2.4.1 반복 영역 건너뛰기 / 5.2.4.2 제목 제공).
|
|
126
|
+
role="region"
|
|
127
|
+
aria-labelledby={titleId}
|
|
118
128
|
width={PALETTE_WIDTH}
|
|
119
129
|
mt={4}
|
|
120
130
|
mb={4}
|
|
@@ -140,7 +150,7 @@ export function NodePalette({ onClose }: { onClose: () => void }) {
|
|
|
140
150
|
borderBottom="1px solid"
|
|
141
151
|
borderColor="slate.200"
|
|
142
152
|
>
|
|
143
|
-
<Subtitle color="slate.1300">
|
|
153
|
+
<Subtitle id={titleId} color="slate.1300">
|
|
144
154
|
{translate('workflow_palette_title')}
|
|
145
155
|
</Subtitle>
|
|
146
156
|
<IconButton
|