@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.
@@ -56,21 +56,49 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyle)(_styles.default), _d
56
56
  constructor(...args) {
57
57
  super(...args);
58
58
  this.ref = null;
59
+ // Reference to hidden aria-live region for announcing caption changes to screen readers
60
+ this._liveRegionRef = null;
61
+ // Timeout for delayed announcement (workaround for Safari/VoiceOver caption update bug)
62
+ this._announcementTimeout = void 0;
59
63
  this.handleRef = el => {
60
- const elementRef = this.props.elementRef;
64
+ var _this$props$elementRe, _this$props;
61
65
  this.ref = el;
62
- if (typeof elementRef === 'function') {
63
- elementRef(el);
64
- }
66
+ (_this$props$elementRe = (_this$props = this.props).elementRef) === null || _this$props$elementRe === void 0 ? void 0 : _this$props$elementRe.call(_this$props, el);
65
67
  };
66
68
  }
67
69
  componentDidMount() {
68
- var _this$props$makeStyle, _this$props;
69
- (_this$props$makeStyle = (_this$props = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props);
70
+ var _this$props$makeStyle, _this$props2;
71
+ (_this$props$makeStyle = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props2);
72
+ }
73
+ componentDidUpdate(prevProps) {
74
+ var _this$props$makeStyle2, _this$props3;
75
+ (_this$props$makeStyle2 = (_this$props3 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props3);
76
+ // Announce caption changes for screen readers (especially VoiceOver)
77
+ // Safari/VoiceOver has a known bug where dynamic <caption> updates aren't announced,
78
+ // so we use an aria-live region as a workaround
79
+ const prevSortInfo = this.getSortedHeaderInfo(prevProps);
80
+ const currentSortInfo = this.getSortedHeaderInfo(this.props);
81
+ // Only announce if sorting actually changed
82
+ 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);
83
+ if (sortingChanged && currentSortInfo && this._liveRegionRef) {
84
+ // Clear any pending announcement
85
+ clearTimeout(this._announcementTimeout);
86
+ // Clear the live region first (part of the clear-then-set pattern)
87
+ this._liveRegionRef.textContent = '';
88
+ // Wait 100ms before setting new content to ensure screen readers detect the change
89
+ this._announcementTimeout = setTimeout(() => {
90
+ if (this._liveRegionRef) {
91
+ const currentCaption = this.getCaptionText(this.props);
92
+ // Append non-breaking space (\u00A0) to force Safari/VoiceOver to treat
93
+ // repeated captions as different announcements
94
+ this._liveRegionRef.textContent = currentCaption + '\u00A0';
95
+ }
96
+ }, 100);
97
+ }
70
98
  }
71
- componentDidUpdate() {
72
- var _this$props$makeStyle2, _this$props2;
73
- (_this$props$makeStyle2 = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props2);
99
+ componentWillUnmount() {
100
+ // Clean up pending announcement timeout
101
+ clearTimeout(this._announcementTimeout);
74
102
  }
75
103
  getHeaders() {
76
104
  const _Children$toArray = _react.Children.toArray(this.props.children),
@@ -86,27 +114,64 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyle)(_styles.default), _d
86
114
  return colHeader.props.children;
87
115
  });
88
116
  }
117
+ getSortedHeaderInfo(props) {
118
+ const _Children$toArray5 = _react.Children.toArray(props.children),
119
+ _Children$toArray6 = (0, _slicedToArray2.default)(_Children$toArray5, 1),
120
+ headChild = _Children$toArray6[0];
121
+ const _Children$toArray7 = _react.Children.toArray(/*#__PURE__*/(0, _react.isValidElement)(headChild) ? headChild.props.children : []),
122
+ _Children$toArray8 = (0, _slicedToArray2.default)(_Children$toArray7, 1),
123
+ firstRow = _Children$toArray8[0];
124
+ const colHeaders = _react.Children.toArray(/*#__PURE__*/(0, _react.isValidElement)(firstRow) ? firstRow.props.children : []);
125
+ // Find the column with an active sort direction
126
+ for (const colHeader of colHeaders) {
127
+ if (/*#__PURE__*/(0, _react.isValidElement)(colHeader) && colHeader.props.sortDirection && colHeader.props.sortDirection !== 'none') {
128
+ var _colHeader$props$chil, _colHeader$props$chil2, _colHeader$props$chil3;
129
+ // Extract header text (may be nested in child components)
130
+ 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 : '';
131
+ return {
132
+ header: headerText,
133
+ direction: colHeader.props.sortDirection
134
+ };
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ getCaptionText(props) {
140
+ const sortInfo = this.getSortedHeaderInfo(props);
141
+ const caption = props.caption;
142
+ if (!sortInfo) return caption;
143
+ const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`;
144
+ return caption ? caption + sortText : sortText.trim();
145
+ }
89
146
  render() {
90
- const _this$props3 = this.props,
91
- margin = _this$props3.margin,
92
- layout = _this$props3.layout,
93
- caption = _this$props3.caption,
94
- children = _this$props3.children,
95
- hover = _this$props3.hover,
96
- styles = _this$props3.styles,
97
- minWidth = _this$props3.minWidth;
147
+ const _this$props4 = this.props,
148
+ margin = _this$props4.margin,
149
+ layout = _this$props4.layout,
150
+ caption = _this$props4.caption,
151
+ children = _this$props4.children,
152
+ hover = _this$props4.hover,
153
+ styles = _this$props4.styles,
154
+ minWidth = _this$props4.minWidth;
98
155
  const isStacked = layout === 'stacked';
99
- const headers = isStacked ? this.getHeaders() : void 0;
156
+ const captionText = this.getCaptionText(this.props);
100
157
  if (!caption) {
101
158
  (0, _console.error)(false, `[Table] required prop caption is not set.`);
102
159
  }
103
- return (0, _jsxRuntime.jsx)(_TableContext.default.Provider, {
160
+ return (0, _jsxRuntime.jsxs)(_TableContext.default.Provider, {
104
161
  value: {
105
- isStacked: isStacked,
162
+ isStacked,
106
163
  hover: hover,
107
- headers: headers
164
+ headers: isStacked ? this.getHeaders() : void 0
108
165
  },
109
- children: (0, _jsxRuntime.jsxs)(_latest.View
166
+ children: [(0, _jsxRuntime.jsx)("div", {
167
+ ref: el => {
168
+ this._liveRegionRef = el;
169
+ },
170
+ "aria-live": "polite",
171
+ "aria-atomic": "true",
172
+ role: "status",
173
+ css: styles === null || styles === void 0 ? void 0 : styles.liveRegion
174
+ }), (0, _jsxRuntime.jsxs)(_latest.View
110
175
  // All HTML props, except the ones accepted by `View` and `Table`
111
176
  , {
112
177
  ..._latest.View.omitViewProps((0, _omitProps.omitProps)(this.props, Table.allowedProps), Table),
@@ -116,20 +181,15 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyle)(_styles.default), _d
116
181
  elementRef: this.handleRef,
117
182
  css: styles === null || styles === void 0 ? void 0 : styles.table,
118
183
  role: isStacked ? 'table' : void 0,
119
- "aria-label": isStacked ? caption : void 0,
120
- children: [!isStacked && (0, _jsxRuntime.jsx)("caption", {
184
+ "aria-label": captionText,
185
+ children: [!isStacked && caption && (0, _jsxRuntime.jsx)("caption", {
121
186
  children: (0, _jsxRuntime.jsx)(_ScreenReaderContent.ScreenReaderContent, {
122
- children: caption
187
+ children: captionText
123
188
  })
124
- }), _react.Children.map(children, child => {
125
- if (/*#__PURE__*/(0, _react.isValidElement)(child)) {
126
- return (0, _safeCloneElement.safeCloneElement)(child, {
127
- key: child.props.name
128
- });
129
- }
130
- return child;
131
- })]
132
- })
189
+ }), _react.Children.map(children, child => /*#__PURE__*/(0, _react.isValidElement)(child) ? (0, _safeCloneElement.safeCloneElement)(child, {
190
+ key: child.props.name
191
+ }) : child)]
192
+ })]
133
193
  });
134
194
  }
135
195
  }, _Table.displayName = "Table", _Table.componentId = 'Table', _Table.allowedProps = _props.allowedProps, _Table.defaultProps = {
@@ -58,6 +58,14 @@ const generateStyle = (componentTheme, props, _sharedTokens) => {
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-table",
3
- "version": "11.7.2-snapshot-50",
3
+ "version": "11.7.2-snapshot-52",
4
4
  "description": "A styled HTML table component",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -15,25 +15,25 @@
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
17
  "@babel/runtime": "^7.27.6",
18
- "@instructure/console": "11.7.2-snapshot-50",
19
- "@instructure/emotion": "11.7.2-snapshot-50",
20
- "@instructure/shared-types": "11.7.2-snapshot-50",
21
- "@instructure/ui-icons": "11.7.2-snapshot-50",
22
- "@instructure/ui-a11y-content": "11.7.2-snapshot-50",
23
- "@instructure/ui-react-utils": "11.7.2-snapshot-50",
24
- "@instructure/ui-simple-select": "11.7.2-snapshot-50",
25
- "@instructure/ui-view": "11.7.2-snapshot-50",
26
- "@instructure/ui-utils": "11.7.2-snapshot-50"
18
+ "@instructure/emotion": "11.7.2-snapshot-52",
19
+ "@instructure/ui-a11y-content": "11.7.2-snapshot-52",
20
+ "@instructure/ui-icons": "11.7.2-snapshot-52",
21
+ "@instructure/console": "11.7.2-snapshot-52",
22
+ "@instructure/shared-types": "11.7.2-snapshot-52",
23
+ "@instructure/ui-react-utils": "11.7.2-snapshot-52",
24
+ "@instructure/ui-simple-select": "11.7.2-snapshot-52",
25
+ "@instructure/ui-utils": "11.7.2-snapshot-52",
26
+ "@instructure/ui-view": "11.7.2-snapshot-52"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@testing-library/jest-dom": "^6.6.3",
30
30
  "@testing-library/react": "15.0.7",
31
31
  "@testing-library/user-event": "^14.6.1",
32
32
  "vitest": "^3.2.2",
33
- "@instructure/ui-axe-check": "11.7.2-snapshot-50",
34
- "@instructure/ui-babel-preset": "11.7.2-snapshot-50",
35
- "@instructure/ui-themes": "11.7.2-snapshot-50",
36
- "@instructure/ui-color-utils": "11.7.2-snapshot-50"
33
+ "@instructure/ui-babel-preset": "11.7.2-snapshot-52",
34
+ "@instructure/ui-themes": "11.7.2-snapshot-52",
35
+ "@instructure/ui-color-utils": "11.7.2-snapshot-52",
36
+ "@instructure/ui-axe-check": "11.7.2-snapshot-52"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "react": ">=18 <=19"
@@ -351,10 +351,7 @@ const SortableTable = ({ caption, headers, rows }) => {
351
351
  </View>
352
352
  )}
353
353
 
354
- <Table
355
- caption={`${caption}: sorted by ${sortBy} in ${direction} order`}
356
- {...props}
357
- >
354
+ <Table caption={caption} {...props}>
358
355
  <Table.Head renderSortLabel="Sort by">
359
356
  {renderHeaderRow(direction)}
360
357
  </Table.Head>
@@ -370,13 +367,6 @@ const SortableTable = ({ caption, headers, rows }) => {
370
367
  ))}
371
368
  </Table.Body>
372
369
  </Table>
373
- <Alert
374
- liveRegion={() => document.getElementById('flash-messages')}
375
- liveRegionPoliteness="polite"
376
- screenReaderOnly
377
- >
378
- {`Sorted by ${sortBy} in ${direction} order`}
379
- </Alert>
380
370
  </div>
381
371
  )}
382
372
  </Responsive>
@@ -505,10 +495,7 @@ const SelectableTable = ({
505
495
  <View as="div" padding="small" background="primary-inverse">
506
496
  {`${selected.size} of ${rowIds.length} selected`}
507
497
  </View>
508
- <Table
509
- caption={`${caption}: sorted by ${sortBy} in ${direction} order`}
510
- {...props}
511
- >
498
+ <Table caption={caption} {...props}>
512
499
  <Table.Head
513
500
  renderSortLabel={
514
501
  <ScreenReaderContent>Sort by</ScreenReaderContent>
@@ -685,15 +672,6 @@ const SortableTable = ({ caption, headers, rows, perPage }) => {
685
672
  ascending={ascending}
686
673
  perPage={perPage}
687
674
  />
688
- <Alert
689
- liveRegion={() => document.getElementById('flash-messages')}
690
- liveRegionPoliteness="polite"
691
- screenReaderOnly
692
- >
693
- {`Sorted by ${sortBy} in ${
694
- ascending ? 'ascending' : 'descending'
695
- } order`}
696
- </Alert>
697
675
  </div>
698
676
  )
699
677
  }
@@ -71,23 +71,51 @@ class Table extends Component<TableProps> {
71
71
  static Cell = Cell
72
72
 
73
73
  ref: Element | null = null
74
+ // Reference to hidden aria-live region for announcing caption changes to screen readers
75
+ _liveRegionRef: HTMLDivElement | null = null
76
+ // Timeout for delayed announcement (workaround for Safari/VoiceOver caption update bug)
77
+ _announcementTimeout?: ReturnType<typeof setTimeout>
74
78
 
75
79
  handleRef = (el: Element | null) => {
76
- const { elementRef } = this.props
77
-
78
80
  this.ref = el
79
-
80
- if (typeof elementRef === 'function') {
81
- elementRef(el)
82
- }
81
+ this.props.elementRef?.(el)
83
82
  }
84
83
 
85
84
  componentDidMount() {
86
85
  this.props.makeStyles?.()
87
86
  }
88
87
 
89
- componentDidUpdate() {
88
+ componentDidUpdate(prevProps: TableProps) {
90
89
  this.props.makeStyles?.()
90
+ // Announce caption changes for screen readers (especially VoiceOver)
91
+ // Safari/VoiceOver has a known bug where dynamic <caption> updates aren't announced,
92
+ // so we use an aria-live region as a workaround
93
+ const prevSortInfo = this.getSortedHeaderInfo(prevProps)
94
+ const currentSortInfo = this.getSortedHeaderInfo(this.props)
95
+ // Only announce if sorting actually changed
96
+ const sortingChanged =
97
+ prevSortInfo?.header !== currentSortInfo?.header ||
98
+ prevSortInfo?.direction !== currentSortInfo?.direction
99
+ if (sortingChanged && currentSortInfo && this._liveRegionRef) {
100
+ // Clear any pending announcement
101
+ clearTimeout(this._announcementTimeout)
102
+ // Clear the live region first (part of the clear-then-set pattern)
103
+ this._liveRegionRef.textContent = ''
104
+ // Wait 100ms before setting new content to ensure screen readers detect the change
105
+ this._announcementTimeout = setTimeout(() => {
106
+ if (this._liveRegionRef) {
107
+ const currentCaption = this.getCaptionText(this.props)
108
+ // Append non-breaking space (\u00A0) to force Safari/VoiceOver to treat
109
+ // repeated captions as different announcements
110
+ this._liveRegionRef.textContent = currentCaption + '\u00A0'
111
+ }
112
+ }, 100)
113
+ }
114
+ }
115
+
116
+ componentWillUnmount() {
117
+ // Clean up pending announcement timeout
118
+ clearTimeout(this._announcementTimeout)
91
119
  }
92
120
 
93
121
  getHeaders() {
@@ -106,11 +134,45 @@ class Table extends Component<TableProps> {
106
134
  )
107
135
  }
108
136
 
137
+ getSortedHeaderInfo(props: TableProps) {
138
+ const [headChild] = Children.toArray(props.children)
139
+ const [firstRow] = Children.toArray(
140
+ isValidElement(headChild) ? headChild.props.children : []
141
+ )
142
+ const colHeaders = Children.toArray(
143
+ isValidElement(firstRow) ? firstRow.props.children : []
144
+ )
145
+ // Find the column with an active sort direction
146
+ for (const colHeader of colHeaders) {
147
+ if (
148
+ isValidElement(colHeader) &&
149
+ colHeader.props.sortDirection &&
150
+ colHeader.props.sortDirection !== 'none'
151
+ ) {
152
+ // Extract header text (may be nested in child components)
153
+ const headerText =
154
+ typeof colHeader.props.children === 'string'
155
+ ? colHeader.props.children
156
+ : colHeader.props.children?.props?.children ?? ''
157
+ return { header: headerText, direction: colHeader.props.sortDirection }
158
+ }
159
+ }
160
+ return null
161
+ }
162
+
163
+ getCaptionText(props: TableProps) {
164
+ const sortInfo = this.getSortedHeaderInfo(props)
165
+ const caption = props.caption as string
166
+ if (!sortInfo) return caption
167
+ const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`
168
+ return caption ? caption + sortText : sortText.trim()
169
+ }
170
+
109
171
  render() {
110
172
  const { margin, layout, caption, children, hover, styles, minWidth } =
111
173
  this.props
112
174
  const isStacked = layout === 'stacked'
113
- const headers = isStacked ? this.getHeaders() : undefined
175
+ const captionText = this.getCaptionText(this.props)
114
176
 
115
177
  if (!caption) {
116
178
  error(false, `[Table] required prop caption is not set.`)
@@ -119,11 +181,23 @@ class Table extends Component<TableProps> {
119
181
  return (
120
182
  <TableContext.Provider
121
183
  value={{
122
- isStacked: isStacked,
184
+ isStacked,
123
185
  hover: hover!,
124
- headers: headers
186
+ headers: isStacked ? this.getHeaders() : undefined
125
187
  }}
126
188
  >
189
+ {/* ARIA live region for dynamic sort announcements.
190
+ MUST be outside <table> due to Safari/VoiceOver bug.
191
+ Empty on page load, populated only when sorting changes. */}
192
+ <div
193
+ ref={(el) => {
194
+ this._liveRegionRef = el
195
+ }}
196
+ aria-live="polite"
197
+ aria-atomic="true"
198
+ role="status"
199
+ css={styles?.liveRegion}
200
+ />
127
201
  <View
128
202
  // All HTML props, except the ones accepted by `View` and `Table`
129
203
  {...View.omitViewProps(
@@ -136,21 +210,21 @@ class Table extends Component<TableProps> {
136
210
  elementRef={this.handleRef}
137
211
  css={styles?.table}
138
212
  role={isStacked ? 'table' : undefined}
139
- aria-label={isStacked ? (caption as string) : undefined}
213
+ aria-label={captionText}
140
214
  >
141
- {!isStacked && (
215
+ {/* Caption for visual display and semantic HTML */}
216
+ {!isStacked && caption && (
142
217
  <caption>
143
- <ScreenReaderContent>{caption}</ScreenReaderContent>
218
+ <ScreenReaderContent>{captionText}</ScreenReaderContent>
144
219
  </caption>
145
220
  )}
146
- {Children.map(children, (child) => {
147
- if (isValidElement(child)) {
148
- return safeCloneElement(child, {
149
- key: (child as ReactElement<any>).props.name
150
- })
151
- }
152
- return child
153
- })}
221
+ {Children.map(children, (child) =>
222
+ isValidElement(child)
223
+ ? safeCloneElement(child, {
224
+ key: (child as ReactElement<any>).props.name
225
+ })
226
+ : child
227
+ )}
154
228
  </View>
155
229
  </TableContext.Provider>
156
230
  )
@@ -79,7 +79,7 @@ type TableProps = TableOwnProps &
79
79
  WithStyleProps<TableTheme, TableStyle> &
80
80
  OtherHTMLAttributes<TableOwnProps>
81
81
 
82
- type TableStyle = ComponentStyle<'table'>
82
+ type TableStyle = ComponentStyle<'table' | 'liveRegion'>
83
83
  const allowedProps: AllowedPropKeys = [
84
84
  'caption',
85
85
  'children',
@@ -55,6 +55,14 @@ const generateStyle = (
55
55
  borderSpacing: 0,
56
56
  ...(layout === 'fixed' && { tableLayout: 'fixed' }),
57
57
  caption: { textAlign: 'start' }
58
+ },
59
+ liveRegion: {
60
+ label: 'table__liveRegion',
61
+ position: 'absolute',
62
+ left: '-10000px',
63
+ width: '1px',
64
+ height: '1px',
65
+ overflow: 'hidden'
58
66
  }
59
67
  }
60
68
  }
@@ -351,10 +351,7 @@ const SortableTable = ({ caption, headers, rows }) => {
351
351
  </View>
352
352
  )}
353
353
 
354
- <Table
355
- caption={`${caption}: sorted by ${sortBy} in ${direction} order`}
356
- {...props}
357
- >
354
+ <Table caption={caption} {...props}>
358
355
  <Table.Head renderSortLabel="Sort by">
359
356
  {renderHeaderRow(direction)}
360
357
  </Table.Head>
@@ -370,13 +367,6 @@ const SortableTable = ({ caption, headers, rows }) => {
370
367
  ))}
371
368
  </Table.Body>
372
369
  </Table>
373
- <Alert
374
- liveRegion={() => document.getElementById('flash-messages')}
375
- liveRegionPoliteness="polite"
376
- screenReaderOnly
377
- >
378
- {`Sorted by ${sortBy} in ${direction} order`}
379
- </Alert>
380
370
  </div>
381
371
  )}
382
372
  </Responsive>
@@ -505,10 +495,7 @@ const SelectableTable = ({
505
495
  <View as="div" padding="small" background="primary-inverse">
506
496
  {`${selected.size} of ${rowIds.length} selected`}
507
497
  </View>
508
- <Table
509
- caption={`${caption}: sorted by ${sortBy} in ${direction} order`}
510
- {...props}
511
- >
498
+ <Table caption={caption} {...props}>
512
499
  <Table.Head
513
500
  renderSortLabel={
514
501
  <ScreenReaderContent>Sort by</ScreenReaderContent>
@@ -685,15 +672,6 @@ const SortableTable = ({ caption, headers, rows, perPage }) => {
685
672
  ascending={ascending}
686
673
  perPage={perPage}
687
674
  />
688
- <Alert
689
- liveRegion={() => document.getElementById('flash-messages')}
690
- liveRegionPoliteness="polite"
691
- screenReaderOnly
692
- >
693
- {`Sorted by ${sortBy} in ${
694
- ascending ? 'ascending' : 'descending'
695
- } order`}
696
- </Alert>
697
675
  </div>
698
676
  )
699
677
  }