Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cypress/component/Table.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('<Table/>', () => {
setHoverStateTo?: boolean
hover?: boolean
}) => (
<Table caption="Test table" hover={hover}>
<Table caption={() => 'Test table'} hover={hover}>
<Table.Head>
<Table.Row data-testid="target-row" setHoverStateTo={setHoverStateTo}>
<Table.ColHeader id="foo">ColHeader</Table.ColHeader>
Expand All @@ -54,7 +54,7 @@ describe('<Table/>', () => {
it('can render table head as a combobox when in stacked layout', async () => {
const sortFoo = cy.stub()
cy.mount(
<Table caption="Sortable table" layout="stacked">
<Table caption={() => 'Sortable table'} layout="stacked">
<Table.Head>
<Table.Row>
<Table.ColHeader id="id" onRequestSort={sortFoo}>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-buttons/src/CondensedButton/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ In the following example, CondensedButton is used so that the button content can
---
type: example
---
<Table caption='Tallest Roller Coasters'>
<Table caption={() => 'Tallest Roller Coasters'}>
<Table.Head>
<Table.Row>
<Table.ColHeader id="Roller Coaster">
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-buttons/src/CondensedButton/v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ In the following example, CondensedButton is used so that the button content can
---
type: example
---
<Table caption='Tallest Roller Coasters'>
<Table caption={() => 'Tallest Roller Coasters'}>
<Table.Head>
<Table.Row>
<Table.ColHeader id="Roller Coaster">
Expand Down
8 changes: 4 additions & 4 deletions packages/ui-pages/src/Pages/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ const Example = () => {
<Byline description={user.name}>
<Avatar name={user.name} />
</Byline>
<Table caption="User details">
<Table caption={() => "User details"}>
<Table.Body>
<Table.Row>
<Table.RowHeader>Age</Table.RowHeader>
Expand All @@ -357,7 +357,7 @@ const Example = () => {
</Table.Body>
</Table>
{user.email && (
<Table caption="User details">
<Table caption={() => "User details"}>
<Table.Body>
<Table.Row>
<Table.RowHeader>Email</Table.RowHeader>
Expand All @@ -367,7 +367,7 @@ const Example = () => {
</Table>
)}
{!isNaN(user.spouse) && (
<Table caption="User details">
<Table caption={() => "User details"}>
<Table.Body>
<Table.Row>
<Table.RowHeader>Spouse</Table.RowHeader>
Expand All @@ -379,7 +379,7 @@ const Example = () => {
</Table>
)}
{Array.isArray(user.parents) && (
<Table caption="User details">
<Table caption={() => "User details"}>
<Table.Body>
<Table.Row>
<Table.RowHeader>Parents</Table.RowHeader>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-table/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { Table } from '@instructure/ui-table'

const MyTable = () => {
return (
<Table caption='Top rated movies'>
<Table caption={() => 'Top rated movies'}>
<Table.Head>
<Table.Row>
<Table.ColHeader>Rank</Table.ColHeader>
Expand Down
55 changes: 44 additions & 11 deletions packages/ui-table/src/Table/__tests__/Table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('<Table />', async () => {

const renderTable = (props?: TableProps) =>
render(
<Table caption="Test table" {...props}>
<Table caption={() => 'Test table'} {...props}>
<Table.Head>
<Table.Row>
<Table.ColHeader id="foo">ColHeader</Table.ColHeader>
Expand Down Expand Up @@ -82,7 +82,7 @@ describe('<Table />', async () => {
it('applies a fixed column layout', async () => {
await renderTable({
layout: 'fixed',
caption: 'Test table'
caption: () => 'Test table'
})
const table = screen.getByRole('table')

Expand All @@ -92,7 +92,7 @@ describe('<Table />', async () => {
it('passes hover to table row', async () => {
renderTable({
hover: true,
caption: 'Test table'
caption: () => 'Test table'
})
const tableRows = screen.getAllByRole('row')

Expand Down Expand Up @@ -123,7 +123,7 @@ describe('<Table />', async () => {
it('can render table in stacked layout', async () => {
renderTable({
layout: 'stacked',
caption: 'Test table'
caption: () => 'Test table'
})
const stackedTable = screen.getByRole('table')

Expand All @@ -135,7 +135,7 @@ describe('<Table />', async () => {

it('can handle non-existent head in stacked layout', async () => {
render(
<Table caption="Test table" layout="stacked">
<Table caption={() => 'Test table'} layout="stacked">
<Table.Body></Table.Body>
</Table>
)
Expand All @@ -146,7 +146,7 @@ describe('<Table />', async () => {

it('can handle empty head in stacked layout', async () => {
render(
<Table caption="Test table" layout="stacked">
<Table caption={() => 'Test table'} layout="stacked">
<Table.Head></Table.Head>
</Table>
)
Expand All @@ -157,7 +157,7 @@ describe('<Table />', async () => {

it('can handle invalid header in stacked layout', async () => {
render(
<Table caption="Test table" layout="stacked">
<Table caption={() => 'Test table'} layout="stacked">
<Table.Head>
<Table.Row>
<Table.Cell>Foo</Table.Cell>
Expand Down Expand Up @@ -189,7 +189,7 @@ describe('<Table />', async () => {

it('does not crash for invalid children in stacked layout', async () => {
render(
<Table caption="Test table" layout="stacked">
<Table caption={() => 'Test table'} layout="stacked">
test1
<span>test</span>
{/* @ts-ignore error is normal here */}
Expand Down Expand Up @@ -233,7 +233,7 @@ describe('<Table />', async () => {
layout: TableProps['layout'] = 'auto'
) =>
render(
<Table caption="Sortable table" layout={layout}>
<Table caption={() => 'Sortable table'} layout={layout}>
<Table.Head>
<Table.Row>
<Table.ColHeader id="foo" {...props} {...handlers}>
Expand Down Expand Up @@ -348,6 +348,39 @@ describe('<Table />', async () => {

expect(header).toHaveAttribute('aria-sort', 'descending')
})

it('calls the caption function with the sorted header and direction', async () => {
const caption = vi.fn((header: string, direction: string) =>
header ? `Movies, sorted by ${header} ${direction}` : 'Movies'
)
const { container } = render(
<Table caption={caption}>
<Table.Head>
<Table.Row>
<Table.ColHeader id="foo" sortDirection="ascending">
Foo
</Table.ColHeader>
<Table.ColHeader id="bar">Bar</Table.ColHeader>
</Table.Row>
</Table.Head>
<Table.Body>
<Table.Row></Table.Row>
</Table.Body>
</Table>
)

expect(caption).toHaveBeenCalledWith('Foo', 'ascending')
expect(container.querySelector('caption')).toHaveTextContent(
'Movies, sorted by Foo ascending'
)
})

it('calls the caption function with an empty header and "none" when nothing is sorted', async () => {
const caption = vi.fn(() => 'Movies')
renderTable({ caption } as Partial<TableProps> as TableProps)

expect(caption).toHaveBeenCalledWith('', 'none')
})
})

describe('when using custom components', () => {
Expand All @@ -369,7 +402,7 @@ describe('<Table />', async () => {
}
}
const table = render(
<Table caption="Test custom table">
<Table caption={() => 'Test custom table'}>
<Table.Head>
<Table.Row>
<Table.ColHeader id="foo">ColHeader</Table.ColHeader>
Expand Down Expand Up @@ -410,7 +443,7 @@ describe('<Table />', async () => {
}

const table = render(
<Table caption="Test custom table">
<Table caption={() => 'Test custom table'}>
<Table.Head>
<CustomTableRow>
<CustomTableCell id="foo">ColHeader</CustomTableCell>
Expand Down
23 changes: 16 additions & 7 deletions packages/ui-table/src/Table/v1/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,18 @@ class Table extends Component<TableProps> {
// so we use an aria-live region as a workaround
const prevSortInfo = this.getSortedHeaderInfo(prevProps)
const currentSortInfo = this.getSortedHeaderInfo(this.props)
// Only announce if sorting actually changed
// Only announce if sorting actually changed. A plain ReactNode caption
// ignores the sort state (see `getCaptionText`), so we only announce sort
// changes when `caption` is a function that can describe them.
const sortingChanged =
prevSortInfo?.header !== currentSortInfo?.header ||
prevSortInfo?.direction !== currentSortInfo?.direction
if (sortingChanged && currentSortInfo && this._liveRegionRef) {
if (
sortingChanged &&
currentSortInfo &&
typeof this.props.caption === 'function' &&
this._liveRegionRef
) {
// Clear any pending announcement
clearTimeout(this._announcementTimeout)
// Clear the live region first (part of the clear-then-set pattern)
Expand Down Expand Up @@ -161,11 +168,13 @@ class Table extends Component<TableProps> {
}

getCaptionText(props: TableProps) {
const sortInfo = this.getSortedHeaderInfo(props)
const caption = props.caption as string
if (!sortInfo) return caption
const sortText = ` Sorted by ${sortInfo.header} (${sortInfo.direction})`
return caption ? caption + sortText : sortText.trim()
const { caption } = props
if (typeof caption === 'function') {
const sortInfo = this.getSortedHeaderInfo(props)
return caption(sortInfo?.header ?? '', sortInfo?.direction ?? 'none')
}

return caption as string
}

render() {
Expand Down
12 changes: 11 additions & 1 deletion packages/ui-table/src/Table/v1/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,21 @@ import type { OtherHTMLAttributes, TableTheme } from '@instructure/shared-types'

type RowChild = React.ReactElement<{ children: React.ReactElement }>

type TableCaption = (
sortByHeader: string,
sortDirection: 'none' | 'ascending' | 'descending'
) => string

type TableOwnProps = {
/**
* Provide a screen reader friendly description. Anything passed to this
* prop will be wrapped by `<ScreenReaderContent>` when it is rendered.
*
* A plain `ReactNode` (e.g. a string) is rendered as-is and the sort state
* is ignored. Pass a function (see {@link TableCaption}) to build a
* localized caption that also reflects the current sort state.
*/
caption: React.ReactNode
caption: React.ReactNode | TableCaption
/**
* Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`,
* `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via
Expand Down Expand Up @@ -92,6 +101,7 @@ const allowedProps: AllowedPropKeys = [
export type {
TableProps,
TableStyle,
TableCaption,
// children
RowChild
}
Expand Down
24 changes: 16 additions & 8 deletions packages/ui-table/src/Table/v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const Example = () => {
return (
<div>
{renderOptions()}
<Table caption="Top rated movies" layout={layout} hover={hover}>
<Table caption={() => 'Top rated movies'} layout={layout} hover={hover}>
<Table.Head>
<Table.Row>
<Table.ColHeader id="Rank">Rank</Table.ColHeader>
Expand Down Expand Up @@ -114,7 +114,7 @@ const Example = ({ headers, rows }) => {
>
{({ layout }) => (
<div>
<Table caption="Top rated movies" layout={layout}>
<Table caption={() => 'Top rated movies'} layout={layout}>
<Table.Head>
<Table.Row>
{(headers || []).map(({ id, text, width, textAlign }) => (
Expand Down Expand Up @@ -298,7 +298,7 @@ const SortableTable = ({ caption, headers, rows }) => {
summary="Set text-align for columns"
background="default"
>
<Table caption="Set text-align for columns">
<Table caption={() => 'Set text-align for columns'}>
<Table.Head>{renderHeaderRow()}</Table.Head>
<Table.Body>
<Table.Row>
Expand Down Expand Up @@ -375,7 +375,11 @@ const SortableTable = ({ caption, headers, rows }) => {

render(
<SortableTable
caption="Top rated movies"
caption={(sortBy, sortDirection) =>
sortBy
? `Top rated movies, sorted by ${sortBy} (${sortDirection})`
: 'Top rated movies'
}
headers={[
{
id: 'rank',
Expand Down Expand Up @@ -682,7 +686,11 @@ const renderRating = (rating) => (

render(
<SortableTable
caption="Top rated movies"
caption={(sortBy, sortDirection) =>
sortBy
? `Top rated movies, sorted by ${sortBy} (${sortDirection})`
: 'Top rated movies'
}
headers={[
{
id: 'Rank',
Expand Down Expand Up @@ -809,7 +817,7 @@ const Example = () => {
return (
<div>
{renderOptions()}
<Table caption="Top rated movies" layout={layout} hover={hover}>
<Table caption={() => 'Top rated movies'} layout={layout} hover={hover}>
<Table.Head>
<Table.Row>
<Table.ColHeader id="Rank">Rank</Table.ColHeader>
Expand Down Expand Up @@ -918,7 +926,7 @@ const Example = () => {
return (
<div>
{renderOptions()}
<Table caption="Top rated movies" layout={layout} hover={hover}>
<Table caption={() => 'Top rated movies'} layout={layout} hover={hover}>
<Table.Head>
<CustomTableRow>
<CustomTableCell scope="col">Rank</CustomTableCell>
Expand Down Expand Up @@ -1062,7 +1070,7 @@ const Example = () => {
return (
<div>
{renderOptions()}
<Table caption="Top rated movies" layout={layout} hover={hover}>
<Table caption={() => 'Top rated movies'} layout={layout} hover={hover}>
<Table.Head>
<CustomTableRow>
<CustomTableCell scope="col">Rank</CustomTableCell>
Expand Down
Loading
Loading