@rainbow-oh/yee-c 0.8.0 → 0.8.1

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.
Files changed (101) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +40 -25
  3. package/dist/cjs/Card/card.js +7 -3
  4. package/dist/cjs/Carousel/carousel-item.js +7 -2
  5. package/dist/cjs/Carousel/carousel.js +31 -4
  6. package/dist/cjs/Cascader/cascader.js +1 -1
  7. package/dist/cjs/Cascader/column.js +1 -1
  8. package/dist/cjs/Cascader/node.js +16 -4
  9. package/dist/cjs/Cascader/search-list.js +13 -4
  10. package/dist/cjs/Checkbox/checkbox-group.js +1 -1
  11. package/dist/cjs/Checkbox/checkbox.js +6 -3
  12. package/dist/cjs/Collapse/panel.js +10 -1
  13. package/dist/cjs/Dialog/dialog.js +22 -7
  14. package/dist/cjs/Drawer/drawer.js +14 -5
  15. package/dist/cjs/Menu/menu-item.js +14 -5
  16. package/dist/cjs/Menu/menu.js +10 -2
  17. package/dist/cjs/Pagination/items.js +10 -5
  18. package/dist/cjs/Pagination/pagination.js +9 -3
  19. package/dist/cjs/Radio/radio-group.js +6 -5
  20. package/dist/cjs/Radio/radio.js +39 -29
  21. package/dist/cjs/Rate/rate.js +6 -1
  22. package/dist/cjs/Rate/star.js +7 -3
  23. package/dist/cjs/Steps/step.js +7 -5
  24. package/dist/cjs/Steps/steps.js +1 -1
  25. package/dist/cjs/Tabs/tab-content.js +3 -1
  26. package/dist/cjs/Tabs/tab-header/index.js +3 -1
  27. package/dist/cjs/Tabs/tab-header/tab-header-title.js +3 -1
  28. package/dist/cjs/hooks/useDelayState.js +3 -2
  29. package/dist/cjs/hooks/useFocusManage.d.ts +1 -1
  30. package/dist/cjs/hooks/useFocusManage.js +27 -22
  31. package/dist/cjs/style/themes.css +1 -1
  32. package/dist/cjs/style/variables.css +1 -1
  33. package/dist/cjs/utils/focusStack.d.ts +6 -2
  34. package/dist/cjs/utils/focusStack.js +40 -11
  35. package/dist/cjs/version.d.ts +1 -1
  36. package/dist/cjs/version.js +1 -1
  37. package/dist/esm/Card/card.js +7 -3
  38. package/dist/esm/Carousel/carousel-item.js +7 -2
  39. package/dist/esm/Carousel/carousel.js +31 -4
  40. package/dist/esm/Cascader/cascader.js +3 -1
  41. package/dist/esm/Cascader/column.js +2 -1
  42. package/dist/esm/Cascader/node.js +18 -4
  43. package/dist/esm/Cascader/search-list.js +18 -4
  44. package/dist/esm/Checkbox/checkbox-group.js +3 -1
  45. package/dist/esm/Checkbox/checkbox.js +6 -3
  46. package/dist/esm/Collapse/panel.js +11 -2
  47. package/dist/esm/Dialog/dialog.js +26 -9
  48. package/dist/esm/Drawer/drawer.js +16 -7
  49. package/dist/esm/Menu/menu-item.js +16 -5
  50. package/dist/esm/Menu/menu.js +11 -5
  51. package/dist/esm/Pagination/items.js +14 -5
  52. package/dist/esm/Pagination/pagination.js +16 -4
  53. package/dist/esm/Radio/radio-group.js +5 -4
  54. package/dist/esm/Radio/radio.js +5 -4
  55. package/dist/esm/Rate/rate.js +6 -1
  56. package/dist/esm/Rate/star.js +12 -4
  57. package/dist/esm/Steps/step.js +12 -6
  58. package/dist/esm/Steps/steps.js +3 -1
  59. package/dist/esm/Tabs/tab-content.js +3 -1
  60. package/dist/esm/Tabs/tab-header/index.js +3 -1
  61. package/dist/esm/Tabs/tab-header/tab-header-title.js +3 -1
  62. package/dist/esm/hooks/useDelayState.js +5 -2
  63. package/dist/esm/hooks/useFocusManage.d.ts +1 -1
  64. package/dist/esm/hooks/useFocusManage.js +31 -13
  65. package/dist/esm/style/themes.css +1 -1
  66. package/dist/esm/style/variables.css +1 -1
  67. package/dist/esm/utils/focusStack.d.ts +6 -2
  68. package/dist/esm/utils/focusStack.js +37 -14
  69. package/dist/esm/version.d.ts +1 -1
  70. package/dist/esm/version.js +1 -1
  71. package/dist/umd/yee-c.min.js +1 -1
  72. package/package.json +10 -6
  73. package/src/Card/card.tsx +5 -1
  74. package/src/Carousel/carousel-item.tsx +7 -1
  75. package/src/Carousel/carousel.tsx +30 -3
  76. package/src/Cascader/cascader.tsx +1 -1
  77. package/src/Cascader/column.tsx +1 -1
  78. package/src/Cascader/node.tsx +14 -2
  79. package/src/Cascader/search-list.tsx +11 -2
  80. package/src/Checkbox/checkbox-group.tsx +1 -1
  81. package/src/Checkbox/checkbox.tsx +3 -0
  82. package/src/Collapse/panel.tsx +11 -1
  83. package/src/Dialog/dialog.tsx +31 -6
  84. package/src/Drawer/drawer.tsx +12 -3
  85. package/src/Menu/menu-item.tsx +12 -3
  86. package/src/Menu/menu.tsx +11 -5
  87. package/src/Pagination/items.tsx +9 -4
  88. package/src/Pagination/pagination.tsx +10 -4
  89. package/src/Radio/radio-group.tsx +2 -1
  90. package/src/Radio/radio.tsx +7 -2
  91. package/src/Rate/rate.tsx +5 -0
  92. package/src/Rate/star.tsx +8 -4
  93. package/src/Steps/step.tsx +15 -5
  94. package/src/Steps/steps.tsx +1 -1
  95. package/src/Tabs/tab-content.tsx +2 -0
  96. package/src/Tabs/tab-header/index.tsx +2 -0
  97. package/src/Tabs/tab-header/tab-header-title.tsx +3 -1
  98. package/src/hooks/useDelayState.ts +4 -2
  99. package/src/hooks/useFocusManage.ts +33 -17
  100. package/src/utils/focusStack.ts +55 -12
  101. package/src/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-oh/yee-c",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "yee basic components",
5
5
  "keywords": [
6
6
  "react",
@@ -10,13 +10,13 @@
10
10
  "design-system",
11
11
  "yee"
12
12
  ],
13
- "homepage": "https://github.com/InsureMO/oh-yee/tree/main/packages/yee-c#readme",
13
+ "homepage": "https://github.com/insureMO/oh-yee/tree/main/packages/yee-c#readme",
14
14
  "bugs": {
15
- "url": "https://github.com/InsureMO/oh-yee/issues"
15
+ "url": "https://github.com/insureMO/oh-yee/issues"
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "https://github.com/InsureMO/oh-yee.git",
19
+ "url": "https://github.com/insureMO/oh-yee.git",
20
20
  "directory": "packages/yee-c"
21
21
  },
22
22
  "license": "MIT",
@@ -94,6 +94,7 @@
94
94
  "devDependencies": {
95
95
  "@commitlint/cli": "^17.1.2",
96
96
  "@commitlint/config-conventional": "^17.1.0",
97
+ "@testing-library/react": "16.3.2",
97
98
  "@types/react": "^19.2.7",
98
99
  "@types/react-dom": "^19.2.3",
99
100
  "@types/react-syntax-highlighter": "^15.5.13",
@@ -102,6 +103,7 @@
102
103
  "eslint": "^8.23.0",
103
104
  "father": "^4.1.0",
104
105
  "husky": "^8.0.1",
106
+ "jsdom": "25.0.1",
105
107
  "lint-staged": "^13.0.3",
106
108
  "prettier": "^3.6.2",
107
109
  "prettier-plugin-organize-imports": "^4.3.0",
@@ -109,7 +111,8 @@
109
111
  "react": "^19.2.0",
110
112
  "react-dom": "^19.2.0",
111
113
  "stylelint": "^14.16.1",
112
- "stylelint-order": "^6.0.4"
114
+ "stylelint-order": "^6.0.4",
115
+ "vitest": "3.2.6"
113
116
  },
114
117
  "peerDependencies": {
115
118
  "react": ">=18.0.0",
@@ -120,7 +123,7 @@
120
123
  "registry": "https://registry.npmjs.org/"
121
124
  },
122
125
  "authors": [
123
- "InsureMO"
126
+ "insureMO"
124
127
  ],
125
128
  "scripts": {
126
129
  "build": "node scripts/generate-version.js && rm -rf dist && father build",
@@ -138,6 +141,7 @@
138
141
  "lint:css": "stylelint \"{src,test}/**/*.{css,less}\" --ignore-pattern \"src/style/color.*.css\"",
139
142
  "lint:es": "eslint \"{src,test}/**/*.{js,jsx,ts,tsx}\"",
140
143
  "start": "npm run dev",
144
+ "test": "vitest run",
141
145
  "watch": "father dev"
142
146
  }
143
147
  }
package/src/Card/card.tsx CHANGED
@@ -73,12 +73,12 @@ const Card: FC<CardProps> = (baseprops) => {
73
73
  )}
74
74
  style={styles?.expandIcon}
75
75
  aria-expanded={mergedExpanded}
76
+ aria-label={mergedExpanded ? 'Collapse content' : 'Expand content'}
76
77
  icon={
77
78
  icon ? (
78
79
  icon
79
80
  ) : (
80
81
  <ChevronUp
81
- aria-label="expanded"
82
82
  strokeWidth={1.5}
83
83
  size={16}
84
84
  style={{
@@ -134,6 +134,7 @@ const Card: FC<CardProps> = (baseprops) => {
134
134
  onKeyDown={handleKeyDown}
135
135
  tabIndex={headerClickable ? 0 : undefined}
136
136
  role={headerClickable ? 'button' : undefined}
137
+ aria-expanded={headerClickable ? mergedExpanded : undefined}
137
138
  >
138
139
  {iconPosition === 'left' ? renderExpandIcon() : null}
139
140
  {title ? (
@@ -154,6 +155,9 @@ const Card: FC<CardProps> = (baseprops) => {
154
155
  animate={mergedExpanded ? { height: 'auto' } : { height: 0 }}
155
156
  exit={{ height: 0, display: 'none' }}
156
157
  transition={{ duration: animationDuration }}
158
+ role="region"
159
+ aria-labelledby={title ? headerId : undefined}
160
+ aria-hidden={!mergedExpanded}
157
161
  >
158
162
  {children}
159
163
  </motion.div>
@@ -7,17 +7,23 @@ const CarouselItem = (props: CarouselItemProps) => {
7
7
  const { style, className, children, index } = props;
8
8
  const { prefixCls, current } = React.useContext(CarouselCtx);
9
9
 
10
+ const isActive = index === current - 1;
11
+
10
12
  return (
11
13
  <div
12
14
  className={clsx(
13
15
  `${prefixCls}-item`,
14
16
  {
15
- [`${prefixCls}-item-active`]: index === current - 1,
17
+ [`${prefixCls}-item-active`]: isActive,
16
18
  },
17
19
  className,
18
20
  )}
19
21
  data-index={index}
20
22
  style={style}
23
+ role="tabpanel"
24
+ aria-roledescription="slide"
25
+ aria-label={`Slide ${(index ?? 0) + 1}`}
26
+ aria-hidden={!isActive}
21
27
  >
22
28
  {children}
23
29
  </div>
@@ -173,6 +173,9 @@ const Carousel = React.forwardRef(
173
173
  { [`${prefixCls}-arrows-visible`]: showArrow },
174
174
  className,
175
175
  ),
176
+ role: 'region' as const,
177
+ 'aria-roledescription': 'carousel',
178
+ 'aria-live': autoplay ? ('off' as const) : ('polite' as const),
176
179
  onMouseEnter: handleMouseEnter,
177
180
  onMouseLeave: handleMouseLeave,
178
181
  };
@@ -198,6 +201,15 @@ const Carousel = React.forwardRef(
198
201
  )}
199
202
  style={styles?.prev}
200
203
  onClick={prev}
204
+ role="button"
205
+ aria-label="Previous slide"
206
+ tabIndex={0}
207
+ onKeyDown={(e) => {
208
+ if (e.key === 'Enter' || e.key === ' ') {
209
+ e.preventDefault();
210
+ prev();
211
+ }
212
+ }}
201
213
  >
202
214
  {direction === 'vertical' ? <ChevronUp /> : <ChevronLeft />}
203
215
  </div>
@@ -211,6 +223,15 @@ const Carousel = React.forwardRef(
211
223
  )}
212
224
  style={styles?.next}
213
225
  onClick={next}
226
+ role="button"
227
+ aria-label="Next slide"
228
+ tabIndex={0}
229
+ onKeyDown={(e) => {
230
+ if (e.key === 'Enter' || e.key === ' ') {
231
+ e.preventDefault();
232
+ next();
233
+ }
234
+ }}
214
235
  >
215
236
  {direction === 'vertical' ? <ChevronDown /> : <ChevronRight />}
216
237
  </div>
@@ -228,17 +249,23 @@ const Carousel = React.forwardRef(
228
249
  classNames?.dot,
229
250
  )}
230
251
  style={styles?.dot}
252
+ role="tablist"
253
+ aria-label="Slide controls"
231
254
  >
232
255
  {new Array(React.Children.count(children))
233
256
  .fill(0)
234
257
  .map((i, index: number) => {
258
+ const isActive =
259
+ current === index + 1 ||
260
+ (current === 0 && index === count - 1);
235
261
  return (
236
262
  <li
237
263
  className={clsx({
238
- active:
239
- current === index + 1 ||
240
- (current === 0 && index === count),
264
+ active: isActive,
241
265
  })}
266
+ role="tab"
267
+ aria-selected={isActive}
268
+ aria-label={`Slide ${index + 1}`}
242
269
  tabIndex={0}
243
270
  onClick={() => handleIndicator(index + 1, 'click')}
244
271
  onKeyDown={(e) => {
@@ -446,7 +446,7 @@ const Cascader = (baseprops: CascaderProps) => {
446
446
 
447
447
  const renderPopup = () => {
448
448
  return (
449
- <div className={clsx(prefixCls)}>
449
+ <div className={clsx(prefixCls)} role="group" aria-label="Cascader options">
450
450
  {isFilter ? (
451
451
  <SearchList
452
452
  prefixCls={prefixCls}
@@ -11,7 +11,7 @@ export interface ColumnProps {
11
11
  const Column = (props: ColumnProps) => {
12
12
  const { prefixCls, data, index } = props;
13
13
  return (
14
- <ul className={`${prefixCls}-menu`} role="menu">
14
+ <ul className={`${prefixCls}-menu`} role="listbox" aria-label={`Level ${index + 1} options`}>
15
15
  {Array.isArray(data) &&
16
16
  data.map((item, idx: number) => {
17
17
  return (
@@ -107,12 +107,24 @@ const Node = (props: NodeProps) => {
107
107
  className={clsName}
108
108
  onMouseEnter={handleMouseEnter}
109
109
  onClick={(e: React.MouseEvent<HTMLElement>) => handleClick(false, e)}
110
+ role="option"
111
+ aria-disabled={disabled || undefined}
112
+ aria-selected={checked || expandedPath[level]?.key === uid}
113
+ aria-expanded={isParent ? expandedPath[level]?.key === uid : undefined}
114
+ tabIndex={disabled ? -1 : 0}
115
+ onKeyDown={(e) => {
116
+ if (e.key === 'Enter' || e.key === ' ') {
117
+ e.preventDefault();
118
+ handleClick(false, e as unknown as React.MouseEvent<HTMLElement>);
119
+ }
120
+ }}
110
121
  >
111
122
  {multiple && (
112
123
  <Checkbox
113
124
  checked={isParent ? checkedAll : checked}
114
125
  onChange={isParent ? () => handleClick(true) : undefined}
115
126
  indeterminate={indeterminate}
127
+ aria-label={`Select ${typeof label === 'string' ? label : String(label)}`}
116
128
  />
117
129
  )}
118
130
  <span
@@ -125,9 +137,9 @@ const Node = (props: NodeProps) => {
125
137
  </span>
126
138
 
127
139
  {load === 1 ? (
128
- <Spin size="small" />
140
+ <Spin size="small" aria-label="Loading" />
129
141
  ) : isLeaf === false ? (
130
- <ChevronRight size={20} />
142
+ <ChevronRight size={20} aria-hidden="true" />
131
143
  ) : null}
132
144
  </li>
133
145
  );
@@ -32,7 +32,7 @@ const SearchList = (props: SearchListProps) => {
32
32
  const { prefixCls, items, onClick } = props;
33
33
 
34
34
  return (
35
- <ul className={`${prefixCls}-filter-menu`}>
35
+ <ul className={`${prefixCls}-filter-menu`} role="listbox" aria-label="Search results">
36
36
  {items.length ? (
37
37
  items.map((item) => {
38
38
  const title = item.data.$source?.title || item.data.labelPath.join(' / ');
@@ -43,13 +43,22 @@ const SearchList = (props: SearchListProps) => {
43
43
  title={typeof title === 'string' ? title : String(title)}
44
44
  onClick={() => onClick(item.data)}
45
45
  key={item.data.uid}
46
+ role="option"
47
+ aria-disabled={item.data.$source?.disabled || undefined}
48
+ tabIndex={0}
49
+ onKeyDown={(e) => {
50
+ if (e.key === 'Enter' || e.key === ' ') {
51
+ e.preventDefault();
52
+ onClick(item.data);
53
+ }
54
+ }}
46
55
  >
47
56
  {renderHighlightText(item.matches)}
48
57
  </li>
49
58
  );
50
59
  })
51
60
  ) : (
52
- <p>{'No result'}</p>
61
+ <li role="option" aria-disabled="true">{'No result'}</li>
53
62
  )}
54
63
  </ul>
55
64
  );
@@ -63,7 +63,7 @@ const CheckboxGroup = (baseprops: CheckboxGroupProps) => {
63
63
  };
64
64
 
65
65
  return (
66
- <div className={cls} style={style}>
66
+ <div className={cls} style={style} role="group" aria-disabled={disabled || undefined}>
67
67
  <CheckboxGroupCtx.Provider
68
68
  value={{ name, buttonStyle, onChange: handleChange }}
69
69
  >
@@ -51,6 +51,7 @@ const Checkbox = (baseprops: CheckboxProps) => {
51
51
  className,
52
52
  )}
53
53
  style={style}
54
+ aria-disabled={disabled || undefined}
54
55
  >
55
56
  <span
56
57
  className={clsx(`${prefixCls}`, {
@@ -68,10 +69,12 @@ const Checkbox = (baseprops: CheckboxProps) => {
68
69
  disabled={disabled}
69
70
  checked={mergedChecked}
70
71
  onChange={handleChange}
72
+ aria-checked={indeterminate ? 'mixed' : mergedChecked}
71
73
  />
72
74
  <span
73
75
  className={clsx(`${prefixCls}-inner`, classNames?.inner)}
74
76
  style={styles?.inner}
77
+ aria-hidden="true"
75
78
  ></span>
76
79
  </span>
77
80
  {children ? (
@@ -1,7 +1,7 @@
1
1
  import clsx from 'clsx';
2
2
  import { ChevronRight } from 'lucide-react';
3
3
  import { AnimatePresence, motion } from 'motion/react';
4
- import React, { useContext } from 'react';
4
+ import React, { useContext, useId } from 'react';
5
5
  import Button from '../Button';
6
6
  import { CollapseContext } from './collapse';
7
7
  import type { PanelProps } from './interface';
@@ -12,6 +12,10 @@ const Panel: React.FC<PanelProps & { id: string | number }> = (props) => {
12
12
 
13
13
  const { prefixCls, activeKey, onExpand } = useContext(CollapseContext);
14
14
 
15
+ const panelId = useId();
16
+ const headerId = `${panelId}-header`;
17
+ const contentId = `${panelId}-content`;
18
+
15
19
  const expanded = activeKey.includes(id);
16
20
 
17
21
  const itemClassName = clsx(
@@ -28,7 +32,9 @@ const Panel: React.FC<PanelProps & { id: string | number }> = (props) => {
28
32
  className={clsx(`${prefixCls}-header`, classNames?.header)}
29
33
  role="button"
30
34
  style={styles?.header}
35
+ id={headerId}
31
36
  aria-expanded={expanded}
37
+ aria-controls={contentId}
32
38
  aria-disabled="false"
33
39
  tabIndex={0}
34
40
  onClick={() => onExpand(id)}
@@ -44,6 +50,7 @@ const Panel: React.FC<PanelProps & { id: string | number }> = (props) => {
44
50
  style={styles?.expandIcon}
45
51
  animate={{ rotate: expanded ? 90 : 0 }}
46
52
  transition={{ duration: 0.15 }}
53
+ aria-hidden="true"
47
54
  >
48
55
  <Button icon={<ChevronRight size={16} />} type="text" />
49
56
  </motion.div>
@@ -69,6 +76,9 @@ const Panel: React.FC<PanelProps & { id: string | number }> = (props) => {
69
76
  <motion.div
70
77
  className={clsx(`${prefixCls}-content`, classNames?.content)}
71
78
  style={styles?.content}
79
+ id={contentId}
80
+ role="region"
81
+ aria-labelledby={headerId}
72
82
  initial={{ height: 0, opacity: 0 }}
73
83
  animate={{ height: 'auto', opacity: 1 }}
74
84
  exit={{ height: 0, opacity: 0 }}
@@ -1,7 +1,14 @@
1
1
  import clsx from 'clsx';
2
2
  import { X } from 'lucide-react';
3
3
  import { motion } from 'motion/react';
4
- import React, { useContext, useEffect, useRef, useState } from 'react';
4
+ import React, {
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useId,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
5
12
  import Button from '../Button';
6
13
  import { GlobalContext } from '../Config-Provider';
7
14
  import useEsc from '../hooks/useEsc';
@@ -55,7 +62,17 @@ const Dialog = (baseprops: DialogProps) => {
55
62
  } = props;
56
63
 
57
64
  const [delayOpen, setDelayOpen] = useState(open);
65
+ const [contentElement, setContentElement] = useState<HTMLDivElement | null>(
66
+ null,
67
+ );
58
68
  const contentRef = useRef<HTMLDivElement>(null);
69
+ const setContentRef = useCallback((element: HTMLDivElement | null) => {
70
+ contentRef.current = element;
71
+ setContentElement(element);
72
+ }, []);
73
+ const dialogId = useId();
74
+ const titleId = `${dialogId}-title`;
75
+ const bodyId = `${dialogId}-body`;
59
76
 
60
77
  // When open changes and openResetLocation is true, reset position
61
78
  useEffect(() => {
@@ -71,7 +88,7 @@ const Dialog = (baseprops: DialogProps) => {
71
88
  });
72
89
 
73
90
  // lock focus in the dialog
74
- useLockFocus(contentRef?.current ?? null, delayOpen);
91
+ useLockFocus(contentElement, delayOpen);
75
92
 
76
93
  // enable esc to close dialog
77
94
  useEsc({
@@ -81,8 +98,8 @@ const Dialog = (baseprops: DialogProps) => {
81
98
  },
82
99
  });
83
100
 
84
- // manage focus
85
- useFocusManage(contentRef.current as HTMLElement, delayOpen);
101
+ // manage focus once per open cycle
102
+ useFocusManage(contentElement, open);
86
103
 
87
104
  const cls = clsx(prefixCls, {
88
105
  [`${prefixCls}-open`]: delayOpen,
@@ -143,6 +160,7 @@ const Dialog = (baseprops: DialogProps) => {
143
160
  <div
144
161
  className={clsx(`${prefixCls}-mask`, classNames?.mask)}
145
162
  style={styles?.mask}
163
+ aria-hidden="true"
146
164
  ></div>
147
165
  )}
148
166
  <div
@@ -154,7 +172,11 @@ const Dialog = (baseprops: DialogProps) => {
154
172
  style={{ width: fullscreen ? '100%' : width, ...styles?.content }}
155
173
  tabIndex={0}
156
174
  onClick={(e) => e.stopPropagation()}
157
- ref={contentRef}
175
+ ref={setContentRef}
176
+ role="dialog"
177
+ aria-modal={showMask ? true : undefined}
178
+ aria-labelledby={title ? titleId : undefined}
179
+ aria-describedby={bodyId}
158
180
  >
159
181
  {closable ? (
160
182
  <Button
@@ -175,12 +197,15 @@ const Dialog = (baseprops: DialogProps) => {
175
197
  className={clsx(`${prefixCls}-header`, classNames?.header)}
176
198
  style={styles?.header}
177
199
  >
178
- <div className={`${prefixCls}-title`}>{title}</div>
200
+ <div className={`${prefixCls}-title`} id={titleId}>
201
+ {title}
202
+ </div>
179
203
  </div>
180
204
  ) : null}
181
205
  <div
182
206
  className={clsx(`${prefixCls}-body`, classNames?.body)}
183
207
  style={styles?.body}
208
+ id={bodyId}
184
209
  >
185
210
  {children}
186
211
  </div>
@@ -1,7 +1,7 @@
1
1
  import clsx from 'clsx';
2
2
  import { X } from 'lucide-react';
3
3
  import { motion } from 'motion/react';
4
- import React, { useContext, useRef, useState } from 'react';
4
+ import React, { useContext, useId, useRef, useState } from 'react';
5
5
  import Button from '../Button';
6
6
  import { GlobalContext } from '../Config-Provider';
7
7
  import Portal from '../Portal';
@@ -81,6 +81,9 @@ const Drawer = (baseprops: DrawerProps) => {
81
81
 
82
82
  const [delayOpen, setDelayOpen] = useState(open);
83
83
  const contentRef = useRef<HTMLDivElement>(null);
84
+ const drawerId = useId();
85
+ const titleId = `${drawerId}-title`;
86
+ const bodyId = `${drawerId}-body`;
84
87
 
85
88
  useLockFocus(contentRef.current, delayOpen);
86
89
 
@@ -149,7 +152,7 @@ const Drawer = (baseprops: DrawerProps) => {
149
152
  const renderNode = () => {
150
153
  const { initial, animate, exit } = getAnimate(placement, open);
151
154
  return (
152
- <div {...rest} className={cls} style={style} role="modal">
155
+ <div {...rest} className={cls} style={style}>
153
156
  {showMask && delayOpen && (
154
157
  <motion.div
155
158
  className={clsx(`${prefixCls}-mask`, classNames?.mask)}
@@ -159,6 +162,7 @@ const Drawer = (baseprops: DrawerProps) => {
159
162
  animate={open ? { opacity: 1 } : { opacity: 0 }}
160
163
  exit={{ opacity: 0 }}
161
164
  transition={{ duration: ANIMATION_DURATION }}
165
+ aria-hidden="true"
162
166
  ></motion.div>
163
167
  )}
164
168
  <motion.div
@@ -191,6 +195,10 @@ const Drawer = (baseprops: DrawerProps) => {
191
195
  tabIndex={0}
192
196
  style={styles?.content}
193
197
  ref={contentRef}
198
+ role="dialog"
199
+ aria-modal={showMask ? true : undefined}
200
+ aria-labelledby={title ? titleId : undefined}
201
+ aria-describedby={bodyId}
194
202
  >
195
203
  {renderClose()}
196
204
  {title ? (
@@ -198,12 +206,13 @@ const Drawer = (baseprops: DrawerProps) => {
198
206
  className={clsx(`${prefixCls}-header`, classNames?.header)}
199
207
  style={styles?.header}
200
208
  >
201
- <div className={clsx(`${prefixCls}-title`, classNames?.title)} style={styles?.title}>{title}</div>
209
+ <div className={clsx(`${prefixCls}-title`, classNames?.title)} style={styles?.title} id={titleId}>{title}</div>
202
210
  </div>
203
211
  ) : null}
204
212
  <div
205
213
  className={clsx(`${prefixCls}-body`, classNames?.body)}
206
214
  style={styles?.body}
215
+ id={bodyId}
207
216
  >
208
217
  {children}
209
218
  </div>
@@ -69,6 +69,10 @@ const MenuItem = (props: MenuItemProps) => {
69
69
  style={{ paddingLeft, ...styles?.item }}
70
70
  onClick={handleClick}
71
71
  title={isCollapsed && typeof label === 'string' ? label : undefined}
72
+ role="menuitem"
73
+ aria-disabled={disabled || undefined}
74
+ aria-current={selectedKeys?.includes(key) ? 'page' : undefined}
75
+ tabIndex={disabled ? -1 : 0}
72
76
  >
73
77
  {iconNode}
74
78
  {!isCollapsed && (
@@ -149,7 +153,7 @@ const MenuItem = (props: MenuItemProps) => {
149
153
  if (isCollapsed) return null;
150
154
  if (mode === 'vertical' || mode === 'horizontal') {
151
155
  return (
152
- <span className={`${prefixCls}-item-switch`}>
156
+ <span className={`${prefixCls}-item-switch`} aria-hidden="true">
153
157
  <ChevronRight strokeWidth={2} size={18} />
154
158
  </span>
155
159
  );
@@ -159,6 +163,7 @@ const MenuItem = (props: MenuItemProps) => {
159
163
  className={`${prefixCls}-item-switch`}
160
164
  animate={{ rotateX: opened ? 180 : 0 }}
161
165
  transition={{ duration: 0.15 }}
166
+ aria-hidden="true"
162
167
  >
163
168
  <ChevronDown strokeWidth={2} size={18} />
164
169
  </motion.span>
@@ -167,7 +172,10 @@ const MenuItem = (props: MenuItemProps) => {
167
172
 
168
173
  const triggerNode = (
169
174
  <div
170
- role="submenu"
175
+ role="menuitem"
176
+ aria-haspopup="true"
177
+ aria-expanded={opened}
178
+ aria-disabled={disabled || undefined}
171
179
  className={clsx(`${prefixCls}-submenu-title`, {
172
180
  [`${prefixCls}-submenu-open`]: opened,
173
181
  [`${prefixCls}-submenu-selected`]:
@@ -178,6 +186,7 @@ const MenuItem = (props: MenuItemProps) => {
178
186
  })}
179
187
  style={{ paddingLeft }}
180
188
  onClick={handleExpand}
189
+ tabIndex={disabled ? -1 : 0}
181
190
  title={isCollapsed && typeof label === 'string' ? label : undefined}
182
191
  >
183
192
  {iconNode}
@@ -203,7 +212,7 @@ const MenuItem = (props: MenuItemProps) => {
203
212
  mode === 'horizontal' && !isCollapsed ? 'bottomLeft' : 'rightTop';
204
213
 
205
214
  const childNode = (
206
- <li className={cls}>
215
+ <li className={cls} role="none">
207
216
  {usePopup ? (
208
217
  <Trigger
209
218
  open={openKeys.includes(key)}
package/src/Menu/menu.tsx CHANGED
@@ -40,9 +40,15 @@ const MenuGroup = ({
40
40
  prefixCls: string;
41
41
  renderItems: (items: MenuItemType[]) => React.ReactNode[];
42
42
  }) => (
43
- <li className={`${prefixCls}-item-group`}>
44
- <div className={`${prefixCls}-item-group-title`}>{item.label}</div>
45
- <ul className={`${prefixCls}-item-group-list`}>
43
+ <li className={`${prefixCls}-item-group`} role="none">
44
+ <div className={`${prefixCls}-item-group-title`} role="presentation" id={`${prefixCls}-group-${String(item.label)}`}>
45
+ {item.label}
46
+ </div>
47
+ <ul
48
+ className={`${prefixCls}-item-group-list`}
49
+ role="group"
50
+ aria-labelledby={`${prefixCls}-group-${String(item.label)}`}
51
+ >
46
52
  {renderItems(item.children)}
47
53
  </ul>
48
54
  </li>
@@ -112,11 +118,11 @@ const Menu = (props: InnerMenuProps) => {
112
118
  const rootStyle = root ? { ...style } : {};
113
119
 
114
120
  return (
115
- <ul {...dataAttrs} className={cls} style={rootStyle} role="menu">
121
+ <ul {...dataAttrs} className={cls} style={rootStyle} role={root && mode === 'horizontal' ? 'menubar' : 'menu'}>
116
122
  <MenuCtx.Provider value={{ level, mode, classNames, styles }}>
117
123
  {getChildFromItems(items)}
118
124
  {level === 0 && footer && (
119
- <li className={`${prefixCls}-footer`}>{footer}</li>
125
+ <li className={`${prefixCls}-footer`} role="none">{footer}</li>
120
126
  )}
121
127
  </MenuCtx.Provider>
122
128
  </ul>
@@ -81,18 +81,19 @@ const Items: React.FC<PaginationItemsProps> = (props) => {
81
81
  }
82
82
  }}
83
83
  role="button"
84
+ aria-label="Jump back 7 pages"
84
85
  key="turnleft"
85
86
  >
86
87
  {components?.jumpPrev ?? (
87
88
  <>
88
- <span className={`${prefixCls}-jump-prev-ellipsis`}>
89
+ <span className={`${prefixCls}-jump-prev-ellipsis`} aria-hidden="true">
89
90
  <Ellipsis
90
91
  className="ellipsis"
91
92
  size={18}
92
93
  strokeWidth={1.5}
93
94
  />
94
95
  </span>
95
- <span className={`${prefixCls}-jump-prev-icon`}>
96
+ <span className={`${prefixCls}-jump-prev-icon`} aria-hidden="true">
96
97
  <ChevronsLeft
97
98
  className="turn"
98
99
  size={18}
@@ -116,18 +117,19 @@ const Items: React.FC<PaginationItemsProps> = (props) => {
116
117
  }
117
118
  }}
118
119
  role="button"
120
+ aria-label="Jump forward 7 pages"
119
121
  key="turn-right"
120
122
  >
121
123
  {components?.jumpNext ?? (
122
124
  <>
123
- <span className={`${prefixCls}-jump-next-ellipsis`}>
125
+ <span className={`${prefixCls}-jump-next-ellipsis`} aria-hidden="true">
124
126
  <Ellipsis
125
127
  className="ellipsis"
126
128
  size={18}
127
129
  strokeWidth={1.5}
128
130
  />
129
131
  </span>
130
- <span className={`${prefixCls}-jump-next-icon`}>
132
+ <span className={`${prefixCls}-jump-next-icon`} aria-hidden="true">
131
133
  <ChevronsRight
132
134
  className="turn"
133
135
  size={18}
@@ -159,6 +161,9 @@ const Items: React.FC<PaginationItemsProps> = (props) => {
159
161
  onChange(number as number);
160
162
  }
161
163
  }}
164
+ role="button"
165
+ aria-label={`Page ${number}`}
166
+ aria-current={current === number ? 'page' : undefined}
162
167
  >
163
168
  {number}
164
169
  </li>