@instructure/ui-table 11.7.2-snapshot-50 → 11.7.2-snapshot-52

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/CHANGELOG.md CHANGED
@@ -3,9 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [11.7.2-snapshot-50](https://github.com/instructure/instructure-ui/compare/v11.7.1...v11.7.2-snapshot-50) (2026-04-21)
6
+ ## [11.7.2-snapshot-52](https://github.com/instructure/instructure-ui/compare/v11.7.1...v11.7.2-snapshot-52) (2026-04-22)
7
7
 
8
- **Note:** Version bump only for package @instructure/ui-table
8
+
9
+ ### Features
10
+
11
+ * **ui-table:** add screen reader announcements for column sorting ([6b23662](https://github.com/instructure/instructure-ui/commit/6b236623f3818007f17aa73a124ec5627467ece6))
9
12
 
10
13
 
11
14
 
@@ -51,21 +51,49 @@ let Table = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_clas
51
51
  constructor(...args) {
52
52
  super(...args);
53
53
  this.ref = null;
54
+ // Reference to hidden aria-live region for announcing caption changes to screen readers
55
+ this._liveRegionRef = null;
56
+ // Timeout for delayed announcement (workaround for Safari/VoiceOver caption update bug)
57
+ this._announcementTimeout = void 0;
54
58
  this.handleRef = el => {
55
- const elementRef = this.props.elementRef;
59
+ var _this$props$elementRe, _this$props;
56
60
  this.ref = el;
57
- if (typeof elementRef === 'function') {
58
- elementRef(el);
59
- }
61
+ (_this$props$elementRe = (_this$props = this.props).elementRef) === null || _this$props$elementRe === void 0 ? void 0 : _this$props$elementRe.call(_this$props, el);
60
62
  };
61
63
  }
62
64
  componentDidMount() {
63
- var _this$props$makeStyle, _this$props;
64
- (_this$props$makeStyle = (_this$props = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props);
65
+ var _this$props$makeStyle, _this$props2;
66
+ (_this$props$makeStyle = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props2);
67
+ }
68
+ componentDidUpdate(prevProps) {
69
+ var _this$props$makeStyle2, _this$props3;
70
+ (_this$props$makeStyle2 = (_this$props3 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props3);
71
+ // Announce caption changes for screen readers (especially VoiceOver)
72
+ // Safari/VoiceOver has a known bug where dynamic <caption> updates aren't announced,
73
+ // so we use an aria-live region as a workaround
74
+ const prevSortInfo = this.getSortedHeaderInfo(prevProps);
75
+ const currentSortInfo = this.getSortedHeaderInfo(this.props);
76
+ // Only announce if sorting actually changed
77
+ const sortingChanged = (prevSortInfo === null || prevSortInfo === void 0 ? void 0 : prevSortInfo.header) !== (currentSortInfo === null || currentSortInfo === void 0 ? void 0 : currentSortInfo.header) || (prevSortInfo === null || prevSortInfo === void 0 ? void 0 : prevSortInfo.direction) !== (currentSortInfo === null || currentSortInfo === void 0 ? void 0 : currentSortInfo.direction);
78
+ if (sortingChanged && currentSortInfo && this._liveRegionRef) {
79
+ // Clear any pending announcement
80
+ clearTimeout(this._announcementTimeout);
81
+ // Clear the live region first (part of the clear-then-set pattern)
82
+ this._liveRegionRef.textContent = '';
83
+ // Wait 100ms before setting new content to ensure screen readers detect the change
84
+ this._announcementTimeout = setTimeout(() => {
85
+ if (this._liveRegionRef) {
86
+ const currentCaption = this.getCaptionText(this.props);
87
+ // Append non-breaking space (\u00A0) to force Safari/VoiceOver to treat
88
+ // repeated captions as different announcements
89
+ this._liveRegionRef.textContent = currentCaption + '\u00A0';
90
+ }
91
+ }, 100);
92
+ }
65
93
  }
66
- componentDidUpdate() {
67
- var _this$props$makeStyle2, _this$props2;
68
- (_this$props$makeStyle2 = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props2);
94
+ componentWillUnmount() {
95
+ // Clean up pending announcement timeout
96
+ clearTimeout(this._announcementTimeout);
69
97
  }
70
98
  getHeaders() {
71
99
  const _Children$toArray = Children.toArray(this.props.children),
@@ -81,27 +109,64 @@ let Table = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_clas
81
109
  return colHeader.props.children;
82
110
  });
83
111
  }
112
+ getSortedHeaderInfo(props) {
113
+ const _Children$toArray5 = Children.toArray(props.children),
114
+ _Children$toArray6 = _slicedToArray(_Children$toArray5, 1),
115
+ headChild = _Children$toArray6[0];
116
+ const _Children$toArray7 = Children.toArray(/*#__PURE__*/isValidElement(headChild) ? headChild.props.children : []),
117
+ _Children$toArray8 = _slicedToArray(_Children$toArray7, 1),
118
+ firstRow = _Children$toArray8[0];
119
+ const colHeaders = Children.toArray(/*#__PURE__*/isValidElement(firstRow) ? firstRow.props.children : []);
120
+ // Find the column with an active sort direction
121
+ for (const colHeader of colHeaders) {
122
+ if (/*#__PURE__*/isValidElement(colHeader) && colHeader.props.sortDirection && colHeader.props.sortDirection !== 'none') {
123
+ var _colHeader$props$chil, _colHeader$props$chil2, _colHeader$props$chil3;
124
+ // Extract header text (may be nested in child components)
125
+ const headerText = typeof colHeader.props.children === 'string' ? colHeader.props.children : (_colHeader$props$chil = (_colHeader$props$chil2 = colHeader.props.children) === null || _colHeader$props$chil2 === void 0 ? void 0 : (_colHeader$props$chil3 = _colHeader$props$chil2.props) === null || _colHeader$props$chil3 === void 0 ? void 0 : _colHeader$props$chil3.children) !== null && _colHeader$props$chil !== void 0 ? _colHeader$props$chil : '';
126
+ return {
127
+ header: headerText,
128
+ direction: colHeader.props.sortDirection
129
+ };
130
+ }
131
+ }
132
+ return null;
133
+ }
134
+ getCaptionText(props) {
135
+ const sortInfo = this.getSortedHeaderInfo(props);
136
+ const caption = props.caption;
137
+ if (!sortInfo) return caption;
138
+ const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`;
139
+ return caption ? caption + sortText : sortText.trim();
140
+ }
84
141
  render() {
85
- const _this$props3 = this.props,
86
- margin = _this$props3.margin,
87
- layout = _this$props3.layout,
88
- caption = _this$props3.caption,
89
- children = _this$props3.children,
90
- hover = _this$props3.hover,
91
- styles = _this$props3.styles,
92
- minWidth = _this$props3.minWidth;
142
+ const _this$props4 = this.props,
143
+ margin = _this$props4.margin,
144
+ layout = _this$props4.layout,
145
+ caption = _this$props4.caption,
146
+ children = _this$props4.children,
147
+ hover = _this$props4.hover,
148
+ styles = _this$props4.styles,
149
+ minWidth = _this$props4.minWidth;
93
150
  const isStacked = layout === 'stacked';
94
- const headers = isStacked ? this.getHeaders() : void 0;
151
+ const captionText = this.getCaptionText(this.props);
95
152
  if (!caption) {
96
153
  error(false, `[Table] required prop caption is not set.`);
97
154
  }
98
- return _jsx(TableContext.Provider, {
155
+ return _jsxs(TableContext.Provider, {
99
156
  value: {
100
- isStacked: isStacked,
157
+ isStacked,
101
158
  hover: hover,
102
- headers: headers
159
+ headers: isStacked ? this.getHeaders() : void 0
103
160
  },
104
- children: _jsxs(View
161
+ children: [_jsx("div", {
162
+ ref: el => {
163
+ this._liveRegionRef = el;
164
+ },
165
+ "aria-live": "polite",
166
+ "aria-atomic": "true",
167
+ role: "status",
168
+ css: styles === null || styles === void 0 ? void 0 : styles.liveRegion
169
+ }), _jsxs(View
105
170
  // All HTML props, except the ones accepted by `View` and `Table`
106
171
  , {
107
172
  ...View.omitViewProps(omitProps(this.props, Table.allowedProps), Table),
@@ -111,20 +176,15 @@ let Table = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_clas
111
176
  elementRef: this.handleRef,
112
177
  css: styles === null || styles === void 0 ? void 0 : styles.table,
113
178
  role: isStacked ? 'table' : void 0,
114
- "aria-label": isStacked ? caption : void 0,
115
- children: [!isStacked && _jsx("caption", {
179
+ "aria-label": captionText,
180
+ children: [!isStacked && caption && _jsx("caption", {
116
181
  children: _jsx(ScreenReaderContent, {
117
- children: caption
182
+ children: captionText
118
183
  })
119
- }), Children.map(children, child => {
120
- if (/*#__PURE__*/isValidElement(child)) {
121
- return safeCloneElement(child, {
122
- key: child.props.name
123
- });
124
- }
125
- return child;
126
- })]
127
- })
184
+ }), Children.map(children, child => /*#__PURE__*/isValidElement(child) ? safeCloneElement(child, {
185
+ key: child.props.name
186
+ }) : child)]
187
+ })]
128
188
  });
129
189
  }
130
190
  }, _Table.displayName = "Table", _Table.componentId = 'Table', _Table.allowedProps = allowedProps, _Table.defaultProps = {
@@ -52,6 +52,14 @@ const generateStyle = (componentTheme, props) => {
52
52
  caption: {
53
53
  textAlign: 'start'
54
54
  }
55
+ },
56
+ liveRegion: {
57
+ label: 'table__liveRegion',
58
+ position: 'absolute',
59
+ left: '-10000px',
60
+ width: '1px',
61
+ height: '1px',
62
+ overflow: 'hidden'
55
63
  }
56
64
  };
57
65
  };
@@ -50,21 +50,49 @@ let Table = (_dec = withStyle(generateStyle), _dec(_class = (_Table = class Tabl
50
50
  constructor(...args) {
51
51
  super(...args);
52
52
  this.ref = null;
53
+ // Reference to hidden aria-live region for announcing caption changes to screen readers
54
+ this._liveRegionRef = null;
55
+ // Timeout for delayed announcement (workaround for Safari/VoiceOver caption update bug)
56
+ this._announcementTimeout = void 0;
53
57
  this.handleRef = el => {
54
- const elementRef = this.props.elementRef;
58
+ var _this$props$elementRe, _this$props;
55
59
  this.ref = el;
56
- if (typeof elementRef === 'function') {
57
- elementRef(el);
58
- }
60
+ (_this$props$elementRe = (_this$props = this.props).elementRef) === null || _this$props$elementRe === void 0 ? void 0 : _this$props$elementRe.call(_this$props, el);
59
61
  };
60
62
  }
61
63
  componentDidMount() {
62
- var _this$props$makeStyle, _this$props;
63
- (_this$props$makeStyle = (_this$props = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props);
64
+ var _this$props$makeStyle, _this$props2;
65
+ (_this$props$makeStyle = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props2);
66
+ }
67
+ componentDidUpdate(prevProps) {
68
+ var _this$props$makeStyle2, _this$props3;
69
+ (_this$props$makeStyle2 = (_this$props3 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props3);
70
+ // Announce caption changes for screen readers (especially VoiceOver)
71
+ // Safari/VoiceOver has a known bug where dynamic <caption> updates aren't announced,
72
+ // so we use an aria-live region as a workaround
73
+ const prevSortInfo = this.getSortedHeaderInfo(prevProps);
74
+ const currentSortInfo = this.getSortedHeaderInfo(this.props);
75
+ // Only announce if sorting actually changed
76
+ const sortingChanged = (prevSortInfo === null || prevSortInfo === void 0 ? void 0 : prevSortInfo.header) !== (currentSortInfo === null || currentSortInfo === void 0 ? void 0 : currentSortInfo.header) || (prevSortInfo === null || prevSortInfo === void 0 ? void 0 : prevSortInfo.direction) !== (currentSortInfo === null || currentSortInfo === void 0 ? void 0 : currentSortInfo.direction);
77
+ if (sortingChanged && currentSortInfo && this._liveRegionRef) {
78
+ // Clear any pending announcement
79
+ clearTimeout(this._announcementTimeout);
80
+ // Clear the live region first (part of the clear-then-set pattern)
81
+ this._liveRegionRef.textContent = '';
82
+ // Wait 100ms before setting new content to ensure screen readers detect the change
83
+ this._announcementTimeout = setTimeout(() => {
84
+ if (this._liveRegionRef) {
85
+ const currentCaption = this.getCaptionText(this.props);
86
+ // Append non-breaking space (\u00A0) to force Safari/VoiceOver to treat
87
+ // repeated captions as different announcements
88
+ this._liveRegionRef.textContent = currentCaption + '\u00A0';
89
+ }
90
+ }, 100);
91
+ }
64
92
  }
65
- componentDidUpdate() {
66
- var _this$props$makeStyle2, _this$props2;
67
- (_this$props$makeStyle2 = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props2);
93
+ componentWillUnmount() {
94
+ // Clean up pending announcement timeout
95
+ clearTimeout(this._announcementTimeout);
68
96
  }
69
97
  getHeaders() {
70
98
  const _Children$toArray = Children.toArray(this.props.children),
@@ -80,27 +108,64 @@ let Table = (_dec = withStyle(generateStyle), _dec(_class = (_Table = class Tabl
80
108
  return colHeader.props.children;
81
109
  });
82
110
  }
111
+ getSortedHeaderInfo(props) {
112
+ const _Children$toArray5 = Children.toArray(props.children),
113
+ _Children$toArray6 = _slicedToArray(_Children$toArray5, 1),
114
+ headChild = _Children$toArray6[0];
115
+ const _Children$toArray7 = Children.toArray(/*#__PURE__*/isValidElement(headChild) ? headChild.props.children : []),
116
+ _Children$toArray8 = _slicedToArray(_Children$toArray7, 1),
117
+ firstRow = _Children$toArray8[0];
118
+ const colHeaders = Children.toArray(/*#__PURE__*/isValidElement(firstRow) ? firstRow.props.children : []);
119
+ // Find the column with an active sort direction
120
+ for (const colHeader of colHeaders) {
121
+ if (/*#__PURE__*/isValidElement(colHeader) && colHeader.props.sortDirection && colHeader.props.sortDirection !== 'none') {
122
+ var _colHeader$props$chil, _colHeader$props$chil2, _colHeader$props$chil3;
123
+ // Extract header text (may be nested in child components)
124
+ const headerText = typeof colHeader.props.children === 'string' ? colHeader.props.children : (_colHeader$props$chil = (_colHeader$props$chil2 = colHeader.props.children) === null || _colHeader$props$chil2 === void 0 ? void 0 : (_colHeader$props$chil3 = _colHeader$props$chil2.props) === null || _colHeader$props$chil3 === void 0 ? void 0 : _colHeader$props$chil3.children) !== null && _colHeader$props$chil !== void 0 ? _colHeader$props$chil : '';
125
+ return {
126
+ header: headerText,
127
+ direction: colHeader.props.sortDirection
128
+ };
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ getCaptionText(props) {
134
+ const sortInfo = this.getSortedHeaderInfo(props);
135
+ const caption = props.caption;
136
+ if (!sortInfo) return caption;
137
+ const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`;
138
+ return caption ? caption + sortText : sortText.trim();
139
+ }
83
140
  render() {
84
- const _this$props3 = this.props,
85
- margin = _this$props3.margin,
86
- layout = _this$props3.layout,
87
- caption = _this$props3.caption,
88
- children = _this$props3.children,
89
- hover = _this$props3.hover,
90
- styles = _this$props3.styles,
91
- minWidth = _this$props3.minWidth;
141
+ const _this$props4 = this.props,
142
+ margin = _this$props4.margin,
143
+ layout = _this$props4.layout,
144
+ caption = _this$props4.caption,
145
+ children = _this$props4.children,
146
+ hover = _this$props4.hover,
147
+ styles = _this$props4.styles,
148
+ minWidth = _this$props4.minWidth;
92
149
  const isStacked = layout === 'stacked';
93
- const headers = isStacked ? this.getHeaders() : void 0;
150
+ const captionText = this.getCaptionText(this.props);
94
151
  if (!caption) {
95
152
  error(false, `[Table] required prop caption is not set.`);
96
153
  }
97
- return _jsx(TableContext.Provider, {
154
+ return _jsxs(TableContext.Provider, {
98
155
  value: {
99
- isStacked: isStacked,
156
+ isStacked,
100
157
  hover: hover,
101
- headers: headers
158
+ headers: isStacked ? this.getHeaders() : void 0
102
159
  },
103
- children: _jsxs(View
160
+ children: [_jsx("div", {
161
+ ref: el => {
162
+ this._liveRegionRef = el;
163
+ },
164
+ "aria-live": "polite",
165
+ "aria-atomic": "true",
166
+ role: "status",
167
+ css: styles === null || styles === void 0 ? void 0 : styles.liveRegion
168
+ }), _jsxs(View
104
169
  // All HTML props, except the ones accepted by `View` and `Table`
105
170
  , {
106
171
  ...View.omitViewProps(omitProps(this.props, Table.allowedProps), Table),
@@ -110,20 +175,15 @@ let Table = (_dec = withStyle(generateStyle), _dec(_class = (_Table = class Tabl
110
175
  elementRef: this.handleRef,
111
176
  css: styles === null || styles === void 0 ? void 0 : styles.table,
112
177
  role: isStacked ? 'table' : void 0,
113
- "aria-label": isStacked ? caption : void 0,
114
- children: [!isStacked && _jsx("caption", {
178
+ "aria-label": captionText,
179
+ children: [!isStacked && caption && _jsx("caption", {
115
180
  children: _jsx(ScreenReaderContent, {
116
- children: caption
181
+ children: captionText
117
182
  })
118
- }), Children.map(children, child => {
119
- if (/*#__PURE__*/isValidElement(child)) {
120
- return safeCloneElement(child, {
121
- key: child.props.name
122
- });
123
- }
124
- return child;
125
- })]
126
- })
183
+ }), Children.map(children, child => /*#__PURE__*/isValidElement(child) ? safeCloneElement(child, {
184
+ key: child.props.name
185
+ }) : child)]
186
+ })]
127
187
  });
128
188
  }
129
189
  }, _Table.displayName = "Table", _Table.componentId = 'Table', _Table.allowedProps = allowedProps, _Table.defaultProps = {
@@ -52,6 +52,14 @@ const generateStyle = (componentTheme, props, _sharedTokens) => {
52
52
  caption: {
53
53
  textAlign: 'start'
54
54
  }
55
+ },
56
+ liveRegion: {
57
+ label: 'table__liveRegion',
58
+ position: 'absolute',
59
+ left: '-10000px',
60
+ width: '1px',
61
+ height: '1px',
62
+ overflow: 'hidden'
55
63
  }
56
64
  };
57
65
  };
@@ -57,21 +57,49 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyleLegacy)(_styles.defaul
57
57
  constructor(...args) {
58
58
  super(...args);
59
59
  this.ref = null;
60
+ // Reference to hidden aria-live region for announcing caption changes to screen readers
61
+ this._liveRegionRef = null;
62
+ // Timeout for delayed announcement (workaround for Safari/VoiceOver caption update bug)
63
+ this._announcementTimeout = void 0;
60
64
  this.handleRef = el => {
61
- const elementRef = this.props.elementRef;
65
+ var _this$props$elementRe, _this$props;
62
66
  this.ref = el;
63
- if (typeof elementRef === 'function') {
64
- elementRef(el);
65
- }
67
+ (_this$props$elementRe = (_this$props = this.props).elementRef) === null || _this$props$elementRe === void 0 ? void 0 : _this$props$elementRe.call(_this$props, el);
66
68
  };
67
69
  }
68
70
  componentDidMount() {
69
- var _this$props$makeStyle, _this$props;
70
- (_this$props$makeStyle = (_this$props = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props);
71
+ var _this$props$makeStyle, _this$props2;
72
+ (_this$props$makeStyle = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props2);
73
+ }
74
+ componentDidUpdate(prevProps) {
75
+ var _this$props$makeStyle2, _this$props3;
76
+ (_this$props$makeStyle2 = (_this$props3 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props3);
77
+ // Announce caption changes for screen readers (especially VoiceOver)
78
+ // Safari/VoiceOver has a known bug where dynamic <caption> updates aren't announced,
79
+ // so we use an aria-live region as a workaround
80
+ const prevSortInfo = this.getSortedHeaderInfo(prevProps);
81
+ const currentSortInfo = this.getSortedHeaderInfo(this.props);
82
+ // Only announce if sorting actually changed
83
+ const sortingChanged = (prevSortInfo === null || prevSortInfo === void 0 ? void 0 : prevSortInfo.header) !== (currentSortInfo === null || currentSortInfo === void 0 ? void 0 : currentSortInfo.header) || (prevSortInfo === null || prevSortInfo === void 0 ? void 0 : prevSortInfo.direction) !== (currentSortInfo === null || currentSortInfo === void 0 ? void 0 : currentSortInfo.direction);
84
+ if (sortingChanged && currentSortInfo && this._liveRegionRef) {
85
+ // Clear any pending announcement
86
+ clearTimeout(this._announcementTimeout);
87
+ // Clear the live region first (part of the clear-then-set pattern)
88
+ this._liveRegionRef.textContent = '';
89
+ // Wait 100ms before setting new content to ensure screen readers detect the change
90
+ this._announcementTimeout = setTimeout(() => {
91
+ if (this._liveRegionRef) {
92
+ const currentCaption = this.getCaptionText(this.props);
93
+ // Append non-breaking space (\u00A0) to force Safari/VoiceOver to treat
94
+ // repeated captions as different announcements
95
+ this._liveRegionRef.textContent = currentCaption + '\u00A0';
96
+ }
97
+ }, 100);
98
+ }
71
99
  }
72
- componentDidUpdate() {
73
- var _this$props$makeStyle2, _this$props2;
74
- (_this$props$makeStyle2 = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props2);
100
+ componentWillUnmount() {
101
+ // Clean up pending announcement timeout
102
+ clearTimeout(this._announcementTimeout);
75
103
  }
76
104
  getHeaders() {
77
105
  const _Children$toArray = _react.Children.toArray(this.props.children),
@@ -87,27 +115,64 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyleLegacy)(_styles.defaul
87
115
  return colHeader.props.children;
88
116
  });
89
117
  }
118
+ getSortedHeaderInfo(props) {
119
+ const _Children$toArray5 = _react.Children.toArray(props.children),
120
+ _Children$toArray6 = (0, _slicedToArray2.default)(_Children$toArray5, 1),
121
+ headChild = _Children$toArray6[0];
122
+ const _Children$toArray7 = _react.Children.toArray(/*#__PURE__*/(0, _react.isValidElement)(headChild) ? headChild.props.children : []),
123
+ _Children$toArray8 = (0, _slicedToArray2.default)(_Children$toArray7, 1),
124
+ firstRow = _Children$toArray8[0];
125
+ const colHeaders = _react.Children.toArray(/*#__PURE__*/(0, _react.isValidElement)(firstRow) ? firstRow.props.children : []);
126
+ // Find the column with an active sort direction
127
+ for (const colHeader of colHeaders) {
128
+ if (/*#__PURE__*/(0, _react.isValidElement)(colHeader) && colHeader.props.sortDirection && colHeader.props.sortDirection !== 'none') {
129
+ var _colHeader$props$chil, _colHeader$props$chil2, _colHeader$props$chil3;
130
+ // Extract header text (may be nested in child components)
131
+ const headerText = typeof colHeader.props.children === 'string' ? colHeader.props.children : (_colHeader$props$chil = (_colHeader$props$chil2 = colHeader.props.children) === null || _colHeader$props$chil2 === void 0 ? void 0 : (_colHeader$props$chil3 = _colHeader$props$chil2.props) === null || _colHeader$props$chil3 === void 0 ? void 0 : _colHeader$props$chil3.children) !== null && _colHeader$props$chil !== void 0 ? _colHeader$props$chil : '';
132
+ return {
133
+ header: headerText,
134
+ direction: colHeader.props.sortDirection
135
+ };
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+ getCaptionText(props) {
141
+ const sortInfo = this.getSortedHeaderInfo(props);
142
+ const caption = props.caption;
143
+ if (!sortInfo) return caption;
144
+ const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`;
145
+ return caption ? caption + sortText : sortText.trim();
146
+ }
90
147
  render() {
91
- const _this$props3 = this.props,
92
- margin = _this$props3.margin,
93
- layout = _this$props3.layout,
94
- caption = _this$props3.caption,
95
- children = _this$props3.children,
96
- hover = _this$props3.hover,
97
- styles = _this$props3.styles,
98
- minWidth = _this$props3.minWidth;
148
+ const _this$props4 = this.props,
149
+ margin = _this$props4.margin,
150
+ layout = _this$props4.layout,
151
+ caption = _this$props4.caption,
152
+ children = _this$props4.children,
153
+ hover = _this$props4.hover,
154
+ styles = _this$props4.styles,
155
+ minWidth = _this$props4.minWidth;
99
156
  const isStacked = layout === 'stacked';
100
- const headers = isStacked ? this.getHeaders() : void 0;
157
+ const captionText = this.getCaptionText(this.props);
101
158
  if (!caption) {
102
159
  (0, _console.error)(false, `[Table] required prop caption is not set.`);
103
160
  }
104
- return (0, _jsxRuntime.jsx)(_TableContext.default.Provider, {
161
+ return (0, _jsxRuntime.jsxs)(_TableContext.default.Provider, {
105
162
  value: {
106
- isStacked: isStacked,
163
+ isStacked,
107
164
  hover: hover,
108
- headers: headers
165
+ headers: isStacked ? this.getHeaders() : void 0
109
166
  },
110
- children: (0, _jsxRuntime.jsxs)(_v11_.View
167
+ children: [(0, _jsxRuntime.jsx)("div", {
168
+ ref: el => {
169
+ this._liveRegionRef = el;
170
+ },
171
+ "aria-live": "polite",
172
+ "aria-atomic": "true",
173
+ role: "status",
174
+ css: styles === null || styles === void 0 ? void 0 : styles.liveRegion
175
+ }), (0, _jsxRuntime.jsxs)(_v11_.View
111
176
  // All HTML props, except the ones accepted by `View` and `Table`
112
177
  , {
113
178
  ..._v11_.View.omitViewProps((0, _omitProps.omitProps)(this.props, Table.allowedProps), Table),
@@ -117,20 +182,15 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyleLegacy)(_styles.defaul
117
182
  elementRef: this.handleRef,
118
183
  css: styles === null || styles === void 0 ? void 0 : styles.table,
119
184
  role: isStacked ? 'table' : void 0,
120
- "aria-label": isStacked ? caption : void 0,
121
- children: [!isStacked && (0, _jsxRuntime.jsx)("caption", {
185
+ "aria-label": captionText,
186
+ children: [!isStacked && caption && (0, _jsxRuntime.jsx)("caption", {
122
187
  children: (0, _jsxRuntime.jsx)(_ScreenReaderContent.ScreenReaderContent, {
123
- children: caption
188
+ children: captionText
124
189
  })
125
- }), _react.Children.map(children, child => {
126
- if (/*#__PURE__*/(0, _react.isValidElement)(child)) {
127
- return (0, _safeCloneElement.safeCloneElement)(child, {
128
- key: child.props.name
129
- });
130
- }
131
- return child;
132
- })]
133
- })
190
+ }), _react.Children.map(children, child => /*#__PURE__*/(0, _react.isValidElement)(child) ? (0, _safeCloneElement.safeCloneElement)(child, {
191
+ key: child.props.name
192
+ }) : child)]
193
+ })]
134
194
  });
135
195
  }
136
196
  }, _Table.displayName = "Table", _Table.componentId = 'Table', _Table.allowedProps = _props.allowedProps, _Table.defaultProps = {
@@ -58,6 +58,14 @@ const generateStyle = (componentTheme, props) => {
58
58
  caption: {
59
59
  textAlign: 'start'
60
60
  }
61
+ },
62
+ liveRegion: {
63
+ label: 'table__liveRegion',
64
+ position: 'absolute',
65
+ left: '-10000px',
66
+ width: '1px',
67
+ height: '1px',
68
+ overflow: 'hidden'
61
69
  }
62
70
  };
63
71
  };