@rainbow-oh/yee-c 0.7.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 (127) 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/Form/form-store.d.ts +2 -1
  16. package/dist/cjs/Form/form-store.js +32 -17
  17. package/dist/cjs/Menu/menu-item.js +14 -5
  18. package/dist/cjs/Menu/menu.js +10 -2
  19. package/dist/cjs/Pagination/items.js +10 -5
  20. package/dist/cjs/Pagination/pagination.js +9 -3
  21. package/dist/cjs/Radio/radio-group.js +6 -5
  22. package/dist/cjs/Radio/radio.js +39 -29
  23. package/dist/cjs/Rate/rate.js +6 -1
  24. package/dist/cjs/Rate/star.js +7 -3
  25. package/dist/cjs/Steps/step.js +7 -5
  26. package/dist/cjs/Steps/steps.js +1 -1
  27. package/dist/cjs/Table/hooks/useColumns.js +1 -1
  28. package/dist/cjs/Table/style/index.less +35 -11
  29. package/dist/cjs/Table/table.js +57 -7
  30. package/dist/cjs/Table/tbody/cell.js +1 -1
  31. package/dist/cjs/Table/util/header.d.ts +3 -1
  32. package/dist/cjs/Table/util/header.js +30 -3
  33. package/dist/cjs/Tabs/tab-content.js +3 -1
  34. package/dist/cjs/Tabs/tab-header/index.js +3 -1
  35. package/dist/cjs/Tabs/tab-header/tab-header-title.js +3 -1
  36. package/dist/cjs/hooks/useDelayState.js +3 -2
  37. package/dist/cjs/hooks/useFocusManage.d.ts +1 -1
  38. package/dist/cjs/hooks/useFocusManage.js +27 -22
  39. package/dist/cjs/style/color.css +2 -0
  40. package/dist/cjs/style/themes.css +1 -1
  41. package/dist/cjs/style/variables.css +3 -1
  42. package/dist/cjs/utils/focusStack.d.ts +6 -2
  43. package/dist/cjs/utils/focusStack.js +40 -11
  44. package/dist/cjs/version.d.ts +1 -1
  45. package/dist/cjs/version.js +1 -1
  46. package/dist/esm/Card/card.js +7 -3
  47. package/dist/esm/Carousel/carousel-item.js +7 -2
  48. package/dist/esm/Carousel/carousel.js +31 -4
  49. package/dist/esm/Cascader/cascader.js +3 -1
  50. package/dist/esm/Cascader/column.js +2 -1
  51. package/dist/esm/Cascader/node.js +18 -4
  52. package/dist/esm/Cascader/search-list.js +18 -4
  53. package/dist/esm/Checkbox/checkbox-group.js +3 -1
  54. package/dist/esm/Checkbox/checkbox.js +6 -3
  55. package/dist/esm/Collapse/panel.js +11 -2
  56. package/dist/esm/Dialog/dialog.js +26 -9
  57. package/dist/esm/Drawer/drawer.js +16 -7
  58. package/dist/esm/Form/form-store.d.ts +2 -1
  59. package/dist/esm/Form/form-store.js +40 -19
  60. package/dist/esm/Menu/menu-item.js +16 -5
  61. package/dist/esm/Menu/menu.js +11 -5
  62. package/dist/esm/Pagination/items.js +14 -5
  63. package/dist/esm/Pagination/pagination.js +16 -4
  64. package/dist/esm/Radio/radio-group.js +5 -4
  65. package/dist/esm/Radio/radio.js +5 -4
  66. package/dist/esm/Rate/rate.js +6 -1
  67. package/dist/esm/Rate/star.js +12 -4
  68. package/dist/esm/Steps/step.js +12 -6
  69. package/dist/esm/Steps/steps.js +3 -1
  70. package/dist/esm/Table/hooks/useColumns.js +1 -1
  71. package/dist/esm/Table/style/index.less +35 -11
  72. package/dist/esm/Table/table.js +54 -2
  73. package/dist/esm/Table/tbody/cell.js +1 -1
  74. package/dist/esm/Table/util/header.d.ts +3 -1
  75. package/dist/esm/Table/util/header.js +30 -2
  76. package/dist/esm/Tabs/tab-content.js +3 -1
  77. package/dist/esm/Tabs/tab-header/index.js +3 -1
  78. package/dist/esm/Tabs/tab-header/tab-header-title.js +3 -1
  79. package/dist/esm/hooks/useDelayState.js +5 -2
  80. package/dist/esm/hooks/useFocusManage.d.ts +1 -1
  81. package/dist/esm/hooks/useFocusManage.js +31 -13
  82. package/dist/esm/style/color.css +2 -0
  83. package/dist/esm/style/themes.css +1 -1
  84. package/dist/esm/style/variables.css +3 -1
  85. package/dist/esm/utils/focusStack.d.ts +6 -2
  86. package/dist/esm/utils/focusStack.js +37 -14
  87. package/dist/esm/version.d.ts +1 -1
  88. package/dist/esm/version.js +1 -1
  89. package/dist/umd/yee-c.min.css +3 -3
  90. package/dist/umd/yee-c.min.js +1 -1
  91. package/package.json +10 -6
  92. package/src/Card/card.tsx +5 -1
  93. package/src/Carousel/carousel-item.tsx +7 -1
  94. package/src/Carousel/carousel.tsx +30 -3
  95. package/src/Cascader/cascader.tsx +1 -1
  96. package/src/Cascader/column.tsx +1 -1
  97. package/src/Cascader/node.tsx +14 -2
  98. package/src/Cascader/search-list.tsx +11 -2
  99. package/src/Checkbox/checkbox-group.tsx +1 -1
  100. package/src/Checkbox/checkbox.tsx +3 -0
  101. package/src/Collapse/panel.tsx +11 -1
  102. package/src/Dialog/dialog.tsx +31 -6
  103. package/src/Drawer/drawer.tsx +12 -3
  104. package/src/Form/form-store.ts +42 -15
  105. package/src/Menu/menu-item.tsx +12 -3
  106. package/src/Menu/menu.tsx +11 -5
  107. package/src/Pagination/items.tsx +9 -4
  108. package/src/Pagination/pagination.tsx +10 -4
  109. package/src/Radio/radio-group.tsx +2 -1
  110. package/src/Radio/radio.tsx +7 -2
  111. package/src/Rate/rate.tsx +5 -0
  112. package/src/Rate/star.tsx +8 -4
  113. package/src/Steps/step.tsx +15 -5
  114. package/src/Steps/steps.tsx +1 -1
  115. package/src/Table/hooks/useColumns.ts +1 -1
  116. package/src/Table/style/index.less +35 -11
  117. package/src/Table/table.tsx +60 -1
  118. package/src/Table/tbody/cell.tsx +1 -1
  119. package/src/Table/util/header.ts +48 -2
  120. package/src/Tabs/tab-content.tsx +2 -0
  121. package/src/Tabs/tab-header/index.tsx +2 -0
  122. package/src/Tabs/tab-header/tab-header-title.tsx +3 -1
  123. package/src/hooks/useDelayState.ts +4 -2
  124. package/src/hooks/useFocusManage.ts +33 -17
  125. package/src/style/color.css +2 -0
  126. package/src/utils/focusStack.ts +55 -12
  127. 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.7.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>
@@ -32,6 +32,11 @@ function move<T>(array: T[], from: number, to: number): T[] {
32
32
 
33
33
  const GLOBAL_WATCH_KEY = '__ALL__';
34
34
 
35
+ const isRelatedNamePath = (first: string, second: string): boolean =>
36
+ first === second ||
37
+ first.startsWith(`${second}.`) ||
38
+ second.startsWith(`${first}.`);
39
+
35
40
  export class FormStore {
36
41
  private store: Store = {};
37
42
  private callbacks: Callbacks = {};
@@ -145,6 +150,9 @@ export class FormStore {
145
150
  });
146
151
  }
147
152
 
153
+ const changedKeys = Object.keys(newStore);
154
+ const relatedFieldNames = this.getRelatedFieldNames(changedKeys);
155
+
148
156
  // Validate on onChange and onBlur (async — fire-and-forget so setFieldsValue
149
157
  // stays synchronous for its callers; results surface via onStoreChange).
150
158
  if (
@@ -152,10 +160,10 @@ export class FormStore {
152
160
  trigger === 'onBlur' ||
153
161
  trigger === 'onSubmit'
154
162
  ) {
155
- void this.validateFields(Object.keys(newStore), trigger);
163
+ void this.validateFields(relatedFieldNames, trigger);
156
164
  }
157
165
 
158
- this.refreshField(newStore);
166
+ this.refreshField(newStore, relatedFieldNames);
159
167
  };
160
168
 
161
169
  resetFields = (name?: Name[]) => {
@@ -279,16 +287,22 @@ export class FormStore {
279
287
  return err;
280
288
  };
281
289
 
282
- refreshField = (newStore?: Store) => {
290
+ private getRelatedFieldNames = (changedKeys: string[]): Name[] =>
291
+ Array.from(this.fieldMap.keys()).filter((fieldName) =>
292
+ changedKeys.some((changedKey) =>
293
+ isRelatedNamePath(String(fieldName), changedKey),
294
+ ),
295
+ );
296
+
297
+ refreshField = (newStore?: Store, relatedFieldNames?: Name[]) => {
283
298
  if (newStore) {
284
- const keys = Object.keys(newStore);
285
- keys.forEach((key) => {
286
- const entity = this.fieldMap.get(key);
287
- if (entity) {
288
- entity?.onStoreChange();
289
- }
290
- this.notifyWatchers(key);
299
+ const changedKeys = Object.keys(newStore);
300
+ const fieldNames =
301
+ relatedFieldNames ?? this.getRelatedFieldNames(changedKeys);
302
+ fieldNames.forEach((fieldName) => {
303
+ this.fieldMap.get(fieldName)?.onStoreChange();
291
304
  });
305
+ this.notifyWatchers(changedKeys);
292
306
  } else {
293
307
  this.fieldEntities.forEach((entity) => {
294
308
  entity?.onStoreChange();
@@ -308,14 +322,27 @@ export class FormStore {
308
322
  }
309
323
  this.watchers.get(key)!.add(callback);
310
324
  return () => {
311
- this.watchers.get(key)?.delete(callback);
325
+ const watchers = this.watchers.get(key);
326
+ watchers?.delete(callback);
327
+ if (watchers?.size === 0) {
328
+ this.watchers.delete(key);
329
+ }
312
330
  };
313
331
  };
314
332
 
315
- private notifyWatchers = (changedKey?: string) => {
316
- if (changedKey) {
317
- this.watchers.get(changedKey)?.forEach((cb) => cb());
318
- this.watchers.get(GLOBAL_WATCH_KEY)?.forEach((cb) => cb());
333
+ private notifyWatchers = (changedKeys?: string | string[]) => {
334
+ if (changedKeys) {
335
+ const keys = Array.isArray(changedKeys) ? changedKeys : [changedKeys];
336
+ const callbacks = new Set<() => void>();
337
+ this.watchers.forEach((watchers, watchedKey) => {
338
+ if (
339
+ watchedKey === GLOBAL_WATCH_KEY ||
340
+ keys.some((changedKey) => isRelatedNamePath(watchedKey, changedKey))
341
+ ) {
342
+ watchers.forEach((callback) => callbacks.add(callback));
343
+ }
344
+ });
345
+ callbacks.forEach((callback) => callback());
319
346
  } else {
320
347
  this.watchers.forEach((set) => set.forEach((cb) => cb()));
321
348
  }
@@ -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)}