@instructure/ui-table 11.7.4-snapshot-102 → 11.7.4-snapshot-103

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.4-snapshot-102](https://github.com/instructure/instructure-ui/compare/v11.7.3...v11.7.4-snapshot-102) (2026-07-14)
6
+ ## [11.7.4-snapshot-103](https://github.com/instructure/instructure-ui/compare/v11.7.3...v11.7.4-snapshot-103) (2026-07-14)
7
7
 
8
- **Note:** Version bump only for package @instructure/ui-table
8
+
9
+ ### Bug Fixes
10
+
11
+ * **ui-table:** fix caption for table to be responsive to sorting ([aac2b17](https://github.com/instructure/instructure-ui/commit/aac2b1790f970a1e8e2d959723eb0e4248105dd2))
9
12
 
10
13
 
11
14
 
package/README.md CHANGED
@@ -29,7 +29,7 @@ import { Table } from '@instructure/ui-table'
29
29
 
30
30
  const MyTable = () => {
31
31
  return (
32
- <Table caption='Top rated movies'>
32
+ <Table caption={() => 'Top rated movies'}>
33
33
  <Table.Head>
34
34
  <Table.Row>
35
35
  <Table.ColHeader>Rank</Table.ColHeader>
@@ -80,9 +80,13 @@ let Table = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_clas
80
80
  // so we use an aria-live region as a workaround
81
81
  const prevSortInfo = this.getSortedHeaderInfo(prevProps);
82
82
  const currentSortInfo = this.getSortedHeaderInfo(this.props);
83
- // Only announce if sorting actually changed
83
+ // Only announce if sorting actually changed. A plain ReactNode caption
84
+ // ignores the sort state (see `getCaptionText`), so we only announce sort
85
+ // changes when `caption` is a function that can describe them.
84
86
  const sortingChanged = prevSortInfo?.header !== currentSortInfo?.header || prevSortInfo?.direction !== currentSortInfo?.direction;
85
- if (sortingChanged && currentSortInfo && this._liveRegionRef) {
87
+ if (sortingChanged && currentSortInfo &&
88
+ // typeof this.props.caption === 'function' &&
89
+ this._liveRegionRef) {
86
90
  // Clear any pending announcement
87
91
  clearTimeout(this._announcementTimeout);
88
92
  // Clear the live region first (part of the clear-then-set pattern)
@@ -130,10 +134,14 @@ let Table = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_clas
130
134
  return null;
131
135
  }
132
136
  getCaptionText(props) {
137
+ const {
138
+ caption
139
+ } = props;
133
140
  const sortInfo = this.getSortedHeaderInfo(props);
134
- const caption = props.caption;
135
- if (!sortInfo) return caption;
136
- const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`;
141
+ if (typeof caption === 'function') {
142
+ return caption(sortInfo?.header ?? '', sortInfo?.direction ?? 'none');
143
+ }
144
+ const sortText = ` Sorted by ${sortInfo?.header} (${sortInfo?.direction})`;
137
145
  return caption ? caption + sortText : sortText.trim();
138
146
  }
139
147
  render() {
@@ -130,16 +130,12 @@ let Table = (_dec = withStyleNew(generateStyle), _dec(_class = class Table exten
130
130
  }
131
131
  getCaptionText(props) {
132
132
  const sortInfo = this.getSortedHeaderInfo(props);
133
- const caption = props.caption;
134
- if (!sortInfo) return caption;
135
- const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`;
136
- return caption ? caption + sortText : sortText.trim();
133
+ return props.caption(sortInfo?.header ?? '', sortInfo?.direction ?? 'none');
137
134
  }
138
135
  render() {
139
136
  const {
140
137
  margin,
141
138
  layout,
142
- caption,
143
139
  children,
144
140
  hover,
145
141
  styles,
@@ -147,7 +143,7 @@ let Table = (_dec = withStyleNew(generateStyle), _dec(_class = class Table exten
147
143
  } = this.props;
148
144
  const isStacked = layout === 'stacked';
149
145
  const captionText = this.getCaptionText(this.props);
150
- if (!caption) {
146
+ if (!captionText) {
151
147
  error(false, `[Table] required prop caption is not set.`);
152
148
  }
153
149
  return _jsxs(TableContext.Provider, {
@@ -175,7 +171,7 @@ let Table = (_dec = withStyleNew(generateStyle), _dec(_class = class Table exten
175
171
  css: styles?.table,
176
172
  role: isStacked ? 'table' : undefined,
177
173
  "aria-label": captionText,
178
- children: [!isStacked && caption && _jsx("caption", {
174
+ children: [!isStacked && captionText && _jsx("caption", {
179
175
  children: _jsx(ScreenReaderContent, {
180
176
  children: captionText
181
177
  })
@@ -86,9 +86,13 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyle)(_styles.default, _th
86
86
  // so we use an aria-live region as a workaround
87
87
  const prevSortInfo = this.getSortedHeaderInfo(prevProps);
88
88
  const currentSortInfo = this.getSortedHeaderInfo(this.props);
89
- // Only announce if sorting actually changed
89
+ // Only announce if sorting actually changed. A plain ReactNode caption
90
+ // ignores the sort state (see `getCaptionText`), so we only announce sort
91
+ // changes when `caption` is a function that can describe them.
90
92
  const sortingChanged = prevSortInfo?.header !== currentSortInfo?.header || prevSortInfo?.direction !== currentSortInfo?.direction;
91
- if (sortingChanged && currentSortInfo && this._liveRegionRef) {
93
+ if (sortingChanged && currentSortInfo &&
94
+ // typeof this.props.caption === 'function' &&
95
+ this._liveRegionRef) {
92
96
  // Clear any pending announcement
93
97
  clearTimeout(this._announcementTimeout);
94
98
  // Clear the live region first (part of the clear-then-set pattern)
@@ -136,10 +140,14 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyle)(_styles.default, _th
136
140
  return null;
137
141
  }
138
142
  getCaptionText(props) {
143
+ const {
144
+ caption
145
+ } = props;
139
146
  const sortInfo = this.getSortedHeaderInfo(props);
140
- const caption = props.caption;
141
- if (!sortInfo) return caption;
142
- const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`;
147
+ if (typeof caption === 'function') {
148
+ return caption(sortInfo?.header ?? '', sortInfo?.direction ?? 'none');
149
+ }
150
+ const sortText = ` Sorted by ${sortInfo?.header} (${sortInfo?.direction})`;
143
151
  return caption ? caption + sortText : sortText.trim();
144
152
  }
145
153
  render() {
@@ -136,16 +136,12 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyleNew)(_styles.default),
136
136
  }
137
137
  getCaptionText(props) {
138
138
  const sortInfo = this.getSortedHeaderInfo(props);
139
- const caption = props.caption;
140
- if (!sortInfo) return caption;
141
- const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`;
142
- return caption ? caption + sortText : sortText.trim();
139
+ return props.caption(sortInfo?.header ?? '', sortInfo?.direction ?? 'none');
143
140
  }
144
141
  render() {
145
142
  const {
146
143
  margin,
147
144
  layout,
148
- caption,
149
145
  children,
150
146
  hover,
151
147
  styles,
@@ -153,7 +149,7 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyleNew)(_styles.default),
153
149
  } = this.props;
154
150
  const isStacked = layout === 'stacked';
155
151
  const captionText = this.getCaptionText(this.props);
156
- if (!caption) {
152
+ if (!captionText) {
157
153
  (0, _console.error)(false, `[Table] required prop caption is not set.`);
158
154
  }
159
155
  return (0, _jsxRuntime.jsxs)(_TableContext.default.Provider, {
@@ -181,7 +177,7 @@ let Table = exports.Table = (_dec = (0, _emotion.withStyleNew)(_styles.default),
181
177
  css: styles?.table,
182
178
  role: isStacked ? 'table' : undefined,
183
179
  "aria-label": captionText,
184
- children: [!isStacked && caption && (0, _jsxRuntime.jsx)("caption", {
180
+ children: [!isStacked && captionText && (0, _jsxRuntime.jsx)("caption", {
185
181
  children: (0, _jsxRuntime.jsx)(_ScreenReaderContent.ScreenReaderContent, {
186
182
  children: captionText
187
183
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-table",
3
- "version": "11.7.4-snapshot-102",
3
+ "version": "11.7.4-snapshot-103",
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.29.7",
18
- "@instructure/console": "11.7.4-snapshot-102",
19
- "@instructure/emotion": "11.7.4-snapshot-102",
20
- "@instructure/ui-a11y-content": "11.7.4-snapshot-102",
21
- "@instructure/ui-icons": "11.7.4-snapshot-102",
22
- "@instructure/shared-types": "11.7.4-snapshot-102",
23
- "@instructure/ui-react-utils": "11.7.4-snapshot-102",
24
- "@instructure/ui-simple-select": "11.7.4-snapshot-102",
25
- "@instructure/ui-themes": "11.7.4-snapshot-102",
26
- "@instructure/ui-utils": "11.7.4-snapshot-102",
27
- "@instructure/ui-view": "11.7.4-snapshot-102"
18
+ "@instructure/console": "11.7.4-snapshot-103",
19
+ "@instructure/emotion": "11.7.4-snapshot-103",
20
+ "@instructure/shared-types": "11.7.4-snapshot-103",
21
+ "@instructure/ui-a11y-content": "11.7.4-snapshot-103",
22
+ "@instructure/ui-icons": "11.7.4-snapshot-103",
23
+ "@instructure/ui-simple-select": "11.7.4-snapshot-103",
24
+ "@instructure/ui-themes": "11.7.4-snapshot-103",
25
+ "@instructure/ui-react-utils": "11.7.4-snapshot-103",
26
+ "@instructure/ui-view": "11.7.4-snapshot-103",
27
+ "@instructure/ui-utils": "11.7.4-snapshot-103"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@testing-library/jest-dom": "^6.9.1",
31
31
  "@testing-library/react": "16.3.2",
32
32
  "@testing-library/user-event": "^14.6.1",
33
33
  "vitest": "^4.1.9",
34
- "@instructure/ui-axe-check": "11.7.4-snapshot-102",
35
- "@instructure/ui-color-utils": "11.7.4-snapshot-102",
36
- "@instructure/ui-babel-preset": "11.7.4-snapshot-102"
34
+ "@instructure/ui-axe-check": "11.7.4-snapshot-103",
35
+ "@instructure/ui-babel-preset": "11.7.4-snapshot-103",
36
+ "@instructure/ui-color-utils": "11.7.4-snapshot-103"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "react": ">=18 <=19"
@@ -93,11 +93,18 @@ class Table extends Component<TableProps> {
93
93
  // so we use an aria-live region as a workaround
94
94
  const prevSortInfo = this.getSortedHeaderInfo(prevProps)
95
95
  const currentSortInfo = this.getSortedHeaderInfo(this.props)
96
- // Only announce if sorting actually changed
96
+ // Only announce if sorting actually changed. A plain ReactNode caption
97
+ // ignores the sort state (see `getCaptionText`), so we only announce sort
98
+ // changes when `caption` is a function that can describe them.
97
99
  const sortingChanged =
98
100
  prevSortInfo?.header !== currentSortInfo?.header ||
99
101
  prevSortInfo?.direction !== currentSortInfo?.direction
100
- if (sortingChanged && currentSortInfo && this._liveRegionRef) {
102
+ if (
103
+ sortingChanged &&
104
+ currentSortInfo &&
105
+ // typeof this.props.caption === 'function' &&
106
+ this._liveRegionRef
107
+ ) {
101
108
  // Clear any pending announcement
102
109
  clearTimeout(this._announcementTimeout)
103
110
  // Clear the live region first (part of the clear-then-set pattern)
@@ -154,7 +161,7 @@ class Table extends Component<TableProps> {
154
161
  const headerText =
155
162
  typeof colHeader.props.children === 'string'
156
163
  ? colHeader.props.children
157
- : colHeader.props.children?.props?.children ?? ''
164
+ : (colHeader.props.children?.props?.children ?? '')
158
165
  return { header: headerText, direction: colHeader.props.sortDirection }
159
166
  }
160
167
  }
@@ -162,10 +169,13 @@ class Table extends Component<TableProps> {
162
169
  }
163
170
 
164
171
  getCaptionText(props: TableProps) {
172
+ const { caption } = props
165
173
  const sortInfo = this.getSortedHeaderInfo(props)
166
- const caption = props.caption as string
167
- if (!sortInfo) return caption
168
- const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`
174
+ if (typeof caption === 'function') {
175
+ return caption(sortInfo?.header ?? '', sortInfo?.direction ?? 'none')
176
+ }
177
+ const sortText = ` Sorted by ${sortInfo?.header} (${sortInfo?.direction})`
178
+
169
179
  return caption ? caption + sortText : sortText.trim()
170
180
  }
171
181
 
@@ -31,12 +31,21 @@ import type { OtherHTMLAttributes, TableTheme } from '@instructure/shared-types'
31
31
 
32
32
  type RowChild = React.ReactElement<{ children: React.ReactElement }>
33
33
 
34
+ type TableCaption = (
35
+ sortByHeader: string,
36
+ sortDirection: 'none' | 'ascending' | 'descending'
37
+ ) => string
38
+
34
39
  type TableOwnProps = {
35
40
  /**
36
41
  * Provide a screen reader friendly description. Anything passed to this
37
42
  * prop will be wrapped by `<ScreenReaderContent>` when it is rendered.
43
+ *
44
+ * A plain `ReactNode` (e.g. a string) is rendered as-is and the sort state
45
+ * is ignored. Pass a function (see {@link TableCaption}) to build a
46
+ * localized caption that also reflects the current sort state.
38
47
  */
39
- caption: React.ReactNode
48
+ caption: React.ReactNode | TableCaption
40
49
  /**
41
50
  * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`,
42
51
  * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via
@@ -92,6 +101,7 @@ const allowedProps: AllowedPropKeys = [
92
101
  export type {
93
102
  TableProps,
94
103
  TableStyle,
104
+ TableCaption,
95
105
  // children
96
106
  RowChild
97
107
  }
@@ -54,7 +54,7 @@ const Example = () => {
54
54
  return (
55
55
  <div>
56
56
  {renderOptions()}
57
- <Table caption="Top rated movies" layout={layout} hover={hover}>
57
+ <Table caption={() => 'Top rated movies'} layout={layout} hover={hover}>
58
58
  <Table.Head>
59
59
  <Table.Row>
60
60
  <Table.ColHeader id="Rank">Rank</Table.ColHeader>
@@ -114,7 +114,7 @@ const Example = ({ headers, rows }) => {
114
114
  >
115
115
  {({ layout }) => (
116
116
  <div>
117
- <Table caption="Top rated movies" layout={layout}>
117
+ <Table caption={() => 'Top rated movies'} layout={layout}>
118
118
  <Table.Head>
119
119
  <Table.Row>
120
120
  {(headers || []).map(({ id, text, width, textAlign }) => (
@@ -298,7 +298,7 @@ const SortableTable = ({ caption, headers, rows }) => {
298
298
  summary="Set text-align for columns"
299
299
  background="default"
300
300
  >
301
- <Table caption="Set text-align for columns">
301
+ <Table caption={() => 'Set text-align for columns'}>
302
302
  <Table.Head>{renderHeaderRow()}</Table.Head>
303
303
  <Table.Body>
304
304
  <Table.Row>
@@ -375,7 +375,11 @@ const SortableTable = ({ caption, headers, rows }) => {
375
375
 
376
376
  render(
377
377
  <SortableTable
378
- caption="Top rated movies"
378
+ caption={(sortBy, sortDirection) =>
379
+ sortBy
380
+ ? `Top rated movies, sorted by ${sortBy} (${sortDirection})`
381
+ : 'Top rated movies'
382
+ }
379
383
  headers={[
380
384
  {
381
385
  id: 'rank',
@@ -682,7 +686,11 @@ const renderRating = (rating) => (
682
686
 
683
687
  render(
684
688
  <SortableTable
685
- caption="Top rated movies"
689
+ caption={(sortBy, sortDirection) =>
690
+ sortBy
691
+ ? `Top rated movies, sorted by ${sortBy} (${sortDirection})`
692
+ : 'Top rated movies'
693
+ }
686
694
  headers={[
687
695
  {
688
696
  id: 'Rank',
@@ -809,7 +817,7 @@ const Example = () => {
809
817
  return (
810
818
  <div>
811
819
  {renderOptions()}
812
- <Table caption="Top rated movies" layout={layout} hover={hover}>
820
+ <Table caption={() => 'Top rated movies'} layout={layout} hover={hover}>
813
821
  <Table.Head>
814
822
  <Table.Row>
815
823
  <Table.ColHeader id="Rank">Rank</Table.ColHeader>
@@ -918,7 +926,7 @@ const Example = () => {
918
926
  return (
919
927
  <div>
920
928
  {renderOptions()}
921
- <Table caption="Top rated movies" layout={layout} hover={hover}>
929
+ <Table caption={() => 'Top rated movies'} layout={layout} hover={hover}>
922
930
  <Table.Head>
923
931
  <CustomTableRow>
924
932
  <CustomTableCell scope="col">Rank</CustomTableCell>
@@ -1062,7 +1070,7 @@ const Example = () => {
1062
1070
  return (
1063
1071
  <div>
1064
1072
  {renderOptions()}
1065
- <Table caption="Top rated movies" layout={layout} hover={hover}>
1073
+ <Table caption={() => 'Top rated movies'} layout={layout} hover={hover}>
1066
1074
  <Table.Head>
1067
1075
  <CustomTableRow>
1068
1076
  <CustomTableCell scope="col">Rank</CustomTableCell>
@@ -162,19 +162,15 @@ class Table extends Component<TableProps> {
162
162
 
163
163
  getCaptionText(props: TableProps) {
164
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()
165
+ return props.caption(sortInfo?.header ?? '', sortInfo?.direction ?? 'none')
169
166
  }
170
167
 
171
168
  render() {
172
- const { margin, layout, caption, children, hover, styles, minWidth } =
173
- this.props
169
+ const { margin, layout, children, hover, styles, minWidth } = this.props
174
170
  const isStacked = layout === 'stacked'
175
171
  const captionText = this.getCaptionText(this.props)
176
172
 
177
- if (!caption) {
173
+ if (!captionText) {
178
174
  error(false, `[Table] required prop caption is not set.`)
179
175
  }
180
176
 
@@ -213,7 +209,7 @@ class Table extends Component<TableProps> {
213
209
  aria-label={captionText}
214
210
  >
215
211
  {/* Caption for visual display and semantic HTML */}
216
- {!isStacked && caption && (
212
+ {!isStacked && captionText && (
217
213
  <caption>
218
214
  <ScreenReaderContent>{captionText}</ScreenReaderContent>
219
215
  </caption>
@@ -32,12 +32,20 @@ import type { NewComponentTypes } from '@instructure/ui-themes'
32
32
 
33
33
  type RowChild = React.ReactElement<{ children: React.ReactElement }>
34
34
 
35
+ type TableCaption = (
36
+ sortByHeader: string,
37
+ sortDirection: 'none' | 'ascending' | 'descending'
38
+ ) => string
39
+
35
40
  type TableOwnProps = {
36
41
  /**
37
- * Provide a screen reader friendly description. Anything passed to this
38
- * prop will be wrapped by `<ScreenReaderContent>` when it is rendered.
42
+ * Provide a screen reader friendly description. The returned string is
43
+ * wrapped by `<ScreenReaderContent>` when it is rendered.
44
+ *
45
+ * This is a function so consumers can build a localized caption that also
46
+ * reflects the current sort state. See {@link TableCaption}.
39
47
  */
40
- caption: React.ReactNode
48
+ caption: TableCaption
41
49
  /**
42
50
  * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`,
43
51
  * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via
@@ -93,6 +101,7 @@ const allowedProps: AllowedPropKeys = [
93
101
  export type {
94
102
  TableProps,
95
103
  TableStyle,
104
+ TableCaption,
96
105
  // children
97
106
  RowChild
98
107
  }