Skip to content

Commit cd6d5b7

Browse files
authored
Merge pull request storybookjs#31581 from storybookjs/issue-24580
Controls: Improve the accessibility of the object control
2 parents 48e565a + b82f413 commit cd6d5b7

File tree

4 files changed

+248
-226
lines changed

4 files changed

+248
-226
lines changed

code/addons/docs/src/blocks/controls/Object.tsx

Lines changed: 49 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,12 @@ const Wrapper = styled.div(({ theme }) => ({
2727
'.rejt-tree': {
2828
marginLeft: '1rem',
2929
fontSize: '13px',
30+
listStyleType: 'none',
3031
},
31-
'.rejt-value-node, .rejt-object-node > .rejt-collapsed, .rejt-array-node > .rejt-collapsed, .rejt-object-node > .rejt-not-collapsed, .rejt-array-node > .rejt-not-collapsed':
32-
{
33-
'& > svg': {
34-
opacity: 0,
35-
transition: 'opacity 0.2s',
36-
},
32+
'.rejt-value-node:hover': {
33+
'& > button': {
34+
opacity: 1,
3735
},
38-
'.rejt-value-node:hover, .rejt-object-node:hover > .rejt-collapsed, .rejt-array-node:hover > .rejt-collapsed, .rejt-object-node:hover > .rejt-not-collapsed, .rejt-array-node:hover > .rejt-not-collapsed':
39-
{
40-
'& > svg': {
41-
opacity: 1,
42-
},
43-
},
44-
'.rejt-edit-form button': {
45-
display: 'none',
4636
},
4737
'.rejt-add-form': {
4838
marginLeft: 10,
@@ -57,62 +47,6 @@ const Wrapper = styled.div(({ theme }) => ({
5747
'.rejt-not-collapsed-delimiter': {
5848
lineHeight: '22px',
5949
},
60-
'.rejt-plus-menu': {
61-
marginLeft: 5,
62-
},
63-
'.rejt-object-node > span > *, .rejt-array-node > span > *': {
64-
position: 'relative',
65-
zIndex: 2,
66-
},
67-
'.rejt-object-node, .rejt-array-node': {
68-
position: 'relative',
69-
},
70-
'.rejt-object-node > span:first-of-type::after, .rejt-array-node > span:first-of-type::after, .rejt-collapsed::before, .rejt-not-collapsed::before':
71-
{
72-
content: '""',
73-
position: 'absolute',
74-
top: 0,
75-
display: 'block',
76-
width: '100%',
77-
marginLeft: '-1rem',
78-
padding: '0 4px 0 1rem',
79-
height: 22,
80-
},
81-
'.rejt-collapsed::before, .rejt-not-collapsed::before': {
82-
zIndex: 1,
83-
background: 'transparent',
84-
borderRadius: 4,
85-
transition: 'background 0.2s',
86-
pointerEvents: 'none',
87-
opacity: 0.1,
88-
},
89-
'.rejt-object-node:hover, .rejt-array-node:hover': {
90-
'& > .rejt-collapsed::before, & > .rejt-not-collapsed::before': {
91-
background: theme.color.secondary,
92-
},
93-
},
94-
'.rejt-collapsed::after, .rejt-not-collapsed::after': {
95-
content: '""',
96-
position: 'absolute',
97-
display: 'inline-block',
98-
pointerEvents: 'none',
99-
width: 0,
100-
height: 0,
101-
},
102-
'.rejt-collapsed::after': {
103-
left: -8,
104-
top: 8,
105-
borderTop: '3px solid transparent',
106-
borderBottom: '3px solid transparent',
107-
borderLeft: '3px solid rgba(153,153,153,0.6)',
108-
},
109-
'.rejt-not-collapsed::after': {
110-
left: -10,
111-
top: 10,
112-
borderTop: '3px solid rgba(153,153,153,0.6)',
113-
borderLeft: '3px solid transparent',
114-
borderRight: '3px solid transparent',
115-
},
11650
'.rejt-value': {
11751
display: 'inline-block',
11852
border: '1px solid transparent',
@@ -137,36 +71,37 @@ const ButtonInline = styled.button<{ primary?: boolean }>(({ theme, primary }) =
13771
color: primary ? theme.color.lightest : theme.color.dark,
13872
fontWeight: primary ? 'bold' : 'normal',
13973
cursor: 'pointer',
140-
order: primary ? 'initial' : 9,
14174
}));
14275

143-
const ActionAddIcon = styled(AddIcon)<{ disabled?: boolean }>(({ theme, disabled }) => ({
144-
display: 'inline-block',
76+
const ActionButton = styled.button(({ theme }) => ({
77+
background: 'none',
78+
border: 0,
79+
display: 'inline-flex',
14580
verticalAlign: 'middle',
146-
width: 15,
147-
height: 15,
14881
padding: 3,
14982
marginLeft: 5,
150-
cursor: disabled ? 'not-allowed' : 'pointer',
15183
color: theme.textMutedColor,
152-
'&:hover': disabled ? {} : { color: theme.color.ancillary },
153-
'svg + &': {
154-
marginLeft: 0,
84+
opacity: 0,
85+
transition: 'opacity 0.2s',
86+
cursor: 'pointer',
87+
position: 'relative',
88+
svg: {
89+
width: 9,
90+
height: 9,
15591
},
156-
}));
157-
158-
const ActionSubstractIcon = styled(SubtractIcon)<{ disabled?: boolean }>(({ theme, disabled }) => ({
159-
display: 'inline-block',
160-
verticalAlign: 'middle',
161-
width: 15,
162-
height: 15,
163-
padding: 3,
164-
marginLeft: 5,
165-
cursor: disabled ? 'not-allowed' : 'pointer',
166-
color: theme.textMutedColor,
167-
'&:hover': disabled ? {} : { color: theme.color.negative },
168-
'svg + &': {
169-
marginLeft: 0,
92+
':disabled': {
93+
cursor: 'not-allowed',
94+
},
95+
':hover, :focus-visible': {
96+
opacity: 1,
97+
},
98+
'&:hover:not(:disabled), &:focus-visible:not(:disabled)': {
99+
'&.rejt-plus-menu': {
100+
color: theme.color.ancillary,
101+
},
102+
'&.rejt-minus-menu': {
103+
color: theme.color.negative,
104+
},
170105
},
171106
}));
172107

@@ -220,7 +155,13 @@ const RawInput = styled(Form.Textarea)(({ theme }) => ({
220155
},
221156
}));
222157

223-
const ENTER_EVENT = { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13 };
158+
const ENTER_EVENT = {
159+
bubbles: true,
160+
cancelable: true,
161+
key: 'Enter',
162+
code: 'Enter',
163+
keyCode: 13,
164+
};
224165
const dispatchEnterKey = (event: SyntheticEvent<HTMLInputElement>) => {
225166
event.currentTarget.dispatchEvent(new globalWindow.KeyboardEvent('keydown', ENTER_EVENT));
226167
};
@@ -311,9 +252,12 @@ export const ObjectControl: FC<ObjectProps> = ({ name, value, onChange, argType
311252
<Wrapper aria-readonly={readonly}>
312253
{isObjectOrArray && (
313254
<RawButton
255+
role="switch"
256+
aria-checked={showRaw}
257+
aria-label={`Edit the ${name} properties in text format`}
314258
onClick={(e: SyntheticEvent) => {
315259
e.preventDefault();
316-
setShowRaw((v) => !v);
260+
setShowRaw((isRaw) => !isRaw);
317261
}}
318262
>
319263
{showRaw ? <EyeCloseIcon /> : <EyeIcon />}
@@ -329,14 +273,21 @@ export const ObjectControl: FC<ObjectProps> = ({ name, value, onChange, argType
329273
onFullyUpdate={onChange}
330274
getStyle={getCustomStyleFunction(theme)}
331275
cancelButtonElement={<ButtonInline type="button">Cancel</ButtonInline>}
332-
editButtonElement={<ButtonInline type="submit">Save</ButtonInline>}
333276
addButtonElement={
334277
<ButtonInline type="submit" primary>
335278
Save
336279
</ButtonInline>
337280
}
338-
plusMenuElement={<ActionAddIcon />}
339-
minusMenuElement={<ActionSubstractIcon />}
281+
plusMenuElement={
282+
<ActionButton type="button">
283+
<AddIcon />
284+
</ActionButton>
285+
}
286+
minusMenuElement={
287+
<ActionButton type="button">
288+
<SubtractIcon />
289+
</ActionButton>
290+
}
340291
inputElement={(_: any, __: any, ___: any, key: string) =>
341292
key ? <Input onFocus={selectValue} onBlur={dispatchEnterKey} /> : <Input />
342293
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { ComponentPropsWithoutRef } from 'react';
2+
import React from 'react';
3+
4+
import { styled } from 'storybook/theming';
5+
6+
const Container = styled.div(({ theme }) => ({
7+
position: 'relative',
8+
':hover': {
9+
'& > .rejt-accordion-button::after': {
10+
background: theme.color.secondary,
11+
},
12+
'& > .rejt-accordion-region > :is(.rejt-plus-menu, .rejt-minus-menu)': {
13+
opacity: 1,
14+
},
15+
},
16+
}));
17+
18+
const Trigger = styled.button(({ theme }) => ({
19+
padding: 0,
20+
background: 'transparent',
21+
border: 'none',
22+
marginRight: '3px',
23+
lineHeight: '22px',
24+
color: theme.color.secondary,
25+
'::after': {
26+
content: '""',
27+
position: 'absolute',
28+
top: 0,
29+
display: 'block',
30+
width: '100%',
31+
marginLeft: '-1rem',
32+
height: '22px',
33+
background: 'transparent',
34+
borderRadius: 4,
35+
transition: 'background 0.2s',
36+
opacity: 0.1,
37+
paddingRight: '20px',
38+
},
39+
'::before': {
40+
content: '""',
41+
position: 'absolute',
42+
},
43+
'&[aria-expanded="true"]::before': {
44+
left: -10,
45+
top: 10,
46+
borderTop: '3px solid rgba(153,153,153,0.6)',
47+
borderLeft: '3px solid transparent',
48+
borderRight: '3px solid transparent',
49+
},
50+
'&[aria-expanded="false"]::before': {
51+
left: -8,
52+
top: 8,
53+
borderTop: '3px solid transparent',
54+
borderBottom: '3px solid transparent',
55+
borderLeft: '3px solid rgba(153,153,153,0.6)',
56+
},
57+
}));
58+
59+
const Region = styled.div({
60+
display: 'inline',
61+
});
62+
63+
type AccordionProps = {
64+
name: string;
65+
keyPath: string[];
66+
collapsed: boolean;
67+
deep: number;
68+
} & ComponentPropsWithoutRef<'button'>;
69+
70+
export function JsonNodeAccordion({
71+
children,
72+
name,
73+
collapsed,
74+
keyPath,
75+
deep,
76+
...props
77+
}: AccordionProps) {
78+
const parentPropertyName = keyPath.at(-1) ?? 'root';
79+
80+
const accordionKey = `${parentPropertyName}-${name}-${deep}`;
81+
82+
const ids = {
83+
trigger: `${accordionKey}-trigger`,
84+
region: `${accordionKey}-region`,
85+
};
86+
87+
const containerTag = keyPath.length > 0 ? 'li' : 'div';
88+
89+
return (
90+
<Container as={containerTag}>
91+
<Trigger
92+
type="button"
93+
aria-expanded={!collapsed}
94+
id={ids.trigger}
95+
aria-controls={ids.region}
96+
className="rejt-accordion-button"
97+
{...props}
98+
>
99+
{name} :
100+
</Trigger>
101+
<Region
102+
role="region"
103+
id={ids.region}
104+
aria-labelledby={ids.trigger}
105+
className="rejt-accordion-region"
106+
>
107+
{children}
108+
</Region>
109+
</Container>
110+
);
111+
}

0 commit comments

Comments
 (0)