mirror of
https://github.com/wagtail/wagtail.git
synced 2024-11-25 05:02:57 +01:00
Add optional caption field to TypedTableBlock
- Closes #8507 - Make caption default to "" for tables that predate adding the caption field - Update the html generated in JS so the rendered form matches our new styles - Use a consistent period (full stop) at the end of the help text sentence (including table fields)
This commit is contained in:
parent
0ec50f0d82
commit
a4c18b4957
@ -37,6 +37,7 @@ Changelog
|
||||
* Show character counts on RichTextBlock with `max_length` (Elhussein Almasri)
|
||||
* Move locale selector in generic IndexView to a filter (Sage Abdullah)
|
||||
* Add ability to customise a page's copy form (Neeraj Yetheendran)
|
||||
* Add optional caption field to `TypedTableBlock` (Tommaso Amici, Cynthia Kiser)
|
||||
* Fix: Update system check for overwriting storage backends to recognise the `STORAGES` setting introduced in Django 4.2 (phijma-leukeleu)
|
||||
* Fix: Prevent password change form from raising a validation error when browser autocomplete fills in the "Old password" field (Chiemezuo Akujobi)
|
||||
* Fix: Ensure that the legacy dropdown options, when closed, do not get accidentally clicked by other interactions wide viewports (CheesyPhoenix, Christer Jensen)
|
||||
|
@ -782,6 +782,7 @@
|
||||
* Badr Fourane
|
||||
* Vaishnav Dasari
|
||||
* Aditya
|
||||
* Tommaso Amici
|
||||
|
||||
## Translators
|
||||
|
||||
|
@ -28,7 +28,7 @@ exports[`telepath: wagtail.widgets.TableInput it renders correctly 1`] = `
|
||||
<label class="w-field__label" for="the-id-handsontable-col-caption">Table caption</label>
|
||||
<div class="w-field w-field--char_field w-field--text_input" data-field="">
|
||||
<div class="w-field__help" id="the-id-handsontable-col-caption-helptext" data-field-help="">
|
||||
<div class="help">A heading that identifies the overall topic of the table, and is useful for screen reader users</div>
|
||||
<div class="help">A heading that identifies the overall topic of the table, and is useful for screen reader users.</div>
|
||||
</div>
|
||||
<div class="w-field__input" data-field-input="">
|
||||
<input type="text" id="the-id-handsontable-col-caption" name="handsontable-col-caption" aria-describedby="the-id-handsontable-col-caption-helptext">
|
||||
|
@ -263,7 +263,7 @@ class TableInput {
|
||||
<label class="w-field__label" for="${id}-handsontable-col-caption">${this.strings['Table caption']}</label>
|
||||
<div class="w-field w-field--char_field w-field--text_input" data-field>
|
||||
<div class="w-field__help" id="${id}-handsontable-col-caption-helptext" data-field-help>
|
||||
<div class="help">${this.strings['A heading that identifies the overall topic of the table, and is useful for screen reader users']}</div>
|
||||
<div class="help">${this.strings['A heading that identifies the overall topic of the table, and is useful for screen reader users.']}</div>
|
||||
</div>
|
||||
<div class="w-field__input" data-field-input>
|
||||
<input type="text" id="${id}-handsontable-col-caption" name="handsontable-col-caption" aria-describedby="${id}-handsontable-col-caption-helptext" />
|
||||
|
@ -40,8 +40,8 @@ const TEST_STRINGS = {
|
||||
'Display the first column as a header.',
|
||||
'Table caption': 'Table caption',
|
||||
|
||||
'A heading that identifies the overall topic of the table, and is useful for screen reader users':
|
||||
'A heading that identifies the overall topic of the table, and is useful for screen reader users',
|
||||
'A heading that identifies the overall topic of the table, and is useful for screen reader users.':
|
||||
'A heading that identifies the overall topic of the table, and is useful for screen reader users.',
|
||||
'Table': 'Table',
|
||||
};
|
||||
|
||||
@ -163,7 +163,7 @@ describe('telepath: wagtail.widgets.TableInput', () => {
|
||||
"Affichez la première colonne sous forme d'en-tête.",
|
||||
'Table caption': 'Légende du tableau',
|
||||
|
||||
'A heading that identifies the overall topic of the table, and is useful for screen reader users':
|
||||
'A heading that identifies the overall topic of the table, and is useful for screen reader users.':
|
||||
"Un en-tête qui identifie le sujet général du tableau et qui est utile pour les utilisateurs de lecteurs d'écran",
|
||||
'Table': 'Tableau',
|
||||
};
|
||||
|
@ -2,6 +2,22 @@
|
||||
|
||||
exports[`wagtail.contrib.typed_table_block.blocks.TypedTableBlock it renders correctly 1`] = `
|
||||
"<div class="typed-table-block ">
|
||||
<div class="w-field__wrapper" data-field-wrapper="">
|
||||
<label class="w-field__label" for="mytable-caption">
|
||||
Caption
|
||||
</label>
|
||||
<div class="w-field w-field--char_field w-field--text_input" data-field="">
|
||||
<div class="w-field__help" data-field-help="">
|
||||
<div class="help">
|
||||
A heading that identifies the overall topic of the table, and is useful for screen reader users.
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-field__input" data-field-input="">
|
||||
<input type="text" id="mytable-caption" name="mytable-caption" value="">
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="mytable-column-count" data-column-count="" value="2">
|
||||
<input type="hidden" name="mytable-row-count" data-row-count="" value="2">
|
||||
<div data-deleted-fields=""><input type="hidden" name="mytable-column-0-deleted" value=""><input type="hidden" name="mytable-column-1-deleted" value=""><input type="hidden" name="mytable-row-0-deleted" value=""><input type="hidden" name="mytable-row-1-deleted" value=""></div>
|
||||
@ -92,6 +108,22 @@ exports[`wagtail.contrib.typed_table_block.blocks.TypedTableBlock it renders cor
|
||||
|
||||
exports[`wagtail.contrib.typed_table_block.blocks.TypedTableBlock setError passes error messages to children 1`] = `
|
||||
"<div class="typed-table-block ">
|
||||
<div class="w-field__wrapper" data-field-wrapper="">
|
||||
<label class="w-field__label" for="mytable-caption">
|
||||
Caption
|
||||
</label>
|
||||
<div class="w-field w-field--char_field w-field--text_input" data-field="">
|
||||
<div class="w-field__help" data-field-help="">
|
||||
<div class="help">
|
||||
A heading that identifies the overall topic of the table, and is useful for screen reader users.
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-field__input" data-field-input="">
|
||||
<input type="text" id="mytable-caption" name="mytable-caption" value="">
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="mytable-column-count" data-column-count="" value="2">
|
||||
<input type="hidden" name="mytable-row-count" data-row-count="" value="2">
|
||||
<div data-deleted-fields=""><input type="hidden" name="mytable-column-0-deleted" value=""><input type="hidden" name="mytable-column-1-deleted" value=""><input type="hidden" name="mytable-row-0-deleted" value=""><input type="hidden" name="mytable-row-1-deleted" value=""></div>
|
||||
@ -182,6 +214,22 @@ exports[`wagtail.contrib.typed_table_block.blocks.TypedTableBlock setError passe
|
||||
|
||||
exports[`wagtail.contrib.typed_table_block.blocks.TypedTableBlock setError shows non-block errors 1`] = `
|
||||
"<div class="typed-table-block "><p class="help-block help-critical">This is just generally wrong</p>
|
||||
<div class="w-field__wrapper" data-field-wrapper="">
|
||||
<label class="w-field__label" for="mytable-caption">
|
||||
Caption
|
||||
</label>
|
||||
<div class="w-field w-field--char_field w-field--text_input" data-field="">
|
||||
<div class="w-field__help" data-field-help="">
|
||||
<div class="help">
|
||||
A heading that identifies the overall topic of the table, and is useful for screen reader users.
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-field__input" data-field-input="">
|
||||
<input type="text" id="mytable-caption" name="mytable-caption" value="">
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="mytable-column-count" data-column-count="" value="2">
|
||||
<input type="hidden" name="mytable-row-count" data-row-count="" value="2">
|
||||
<div data-deleted-fields=""><input type="hidden" name="mytable-column-0-deleted" value=""><input type="hidden" name="mytable-column-1-deleted" value=""><input type="hidden" name="mytable-row-0-deleted" value=""><input type="hidden" name="mytable-row-1-deleted" value=""></div>
|
||||
|
@ -13,6 +13,8 @@ export class TypedTableBlock {
|
||||
this.blockDef = blockDef;
|
||||
this.type = blockDef.name;
|
||||
|
||||
this.caption = '';
|
||||
|
||||
// list of column definition objects, each consisting of fields:
|
||||
// * blockDef: the block definition object
|
||||
// * position: the 0-indexed position of this column within the list of columns
|
||||
@ -44,8 +46,25 @@ export class TypedTableBlock {
|
||||
});
|
||||
|
||||
const strings = this.blockDef.meta.strings;
|
||||
const captionID = `${h(prefix)}-caption`;
|
||||
const dom = $(`
|
||||
<div class="typed-table-block ${h(this.blockDef.meta.classname || '')}">
|
||||
<div class="w-field__wrapper" data-field-wrapper>
|
||||
<label class="w-field__label" for="${captionID}">
|
||||
${strings.CAPTION}
|
||||
</label>
|
||||
<div class="w-field w-field--char_field w-field--text_input" data-field>
|
||||
<div class="w-field__help" data-field-help>
|
||||
<div class="help">
|
||||
${strings.CAPTION_HELP_TEXT}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-field__input" data-field-input>
|
||||
<input type="text" id="${captionID}" name="${captionID}" value="" />
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="${h(
|
||||
prefix,
|
||||
)}-column-count" data-column-count value="0">
|
||||
@ -89,6 +108,7 @@ export class TypedTableBlock {
|
||||
`);
|
||||
$(placeholder).replaceWith(dom);
|
||||
this.container = dom;
|
||||
this.captionInput = dom.find(`#${captionID}`).get(0);
|
||||
this.thead = dom.find('table > thead').get(0);
|
||||
this.tbody = dom.find('table > tbody').get(0);
|
||||
|
||||
@ -175,6 +195,7 @@ export class TypedTableBlock {
|
||||
|
||||
clear() {
|
||||
// reset to initial empty state with no rows or columns
|
||||
this.setCaption('');
|
||||
this.columns = [];
|
||||
this.rows = [];
|
||||
this.columnCountIncludingDeleted = 0;
|
||||
@ -202,6 +223,11 @@ export class TypedTableBlock {
|
||||
this.addRowButton.hide();
|
||||
}
|
||||
|
||||
setCaption(caption) {
|
||||
this.caption = caption;
|
||||
this.captionInput.value = caption;
|
||||
}
|
||||
|
||||
insertColumn(index, blockDef, opts) {
|
||||
const column = {
|
||||
blockDef,
|
||||
@ -454,6 +480,7 @@ export class TypedTableBlock {
|
||||
state.rows.forEach((row, index) => {
|
||||
this.insertRow(index, row.values);
|
||||
});
|
||||
this.setCaption(state.caption);
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,6 +510,7 @@ export class TypedTableBlock {
|
||||
rows: this.rows.map((row) => ({
|
||||
values: row.blocks.map((block) => block.getState()),
|
||||
})),
|
||||
caption: this.caption,
|
||||
};
|
||||
return state;
|
||||
}
|
||||
@ -506,6 +534,7 @@ export class TypedTableBlock {
|
||||
rows: this.rows.map((row) => ({
|
||||
values: row.blocks.map((block) => block.getValue()),
|
||||
})),
|
||||
caption: this.caption,
|
||||
};
|
||||
return value;
|
||||
}
|
||||
|
@ -101,6 +101,9 @@ describe('wagtail.contrib.typed_table_block.blocks.TypedTableBlock', () => {
|
||||
helpText: 'use <strong>plenty</strong> of these',
|
||||
helpIcon: '<svg></svg>',
|
||||
strings: {
|
||||
CAPTION: 'Caption',
|
||||
CAPTION_HELP_TEXT:
|
||||
'A heading that identifies the overall topic of the table, and is useful for screen reader users.',
|
||||
ADD_COLUMN: 'Add column',
|
||||
ADD_ROW: 'Add row',
|
||||
COLUMN_HEADING: 'Column heading',
|
||||
@ -123,6 +126,7 @@ describe('wagtail.contrib.typed_table_block.blocks.TypedTableBlock', () => {
|
||||
{ type: 'test_block_b', heading: 'Quantity' },
|
||||
],
|
||||
rows: [{ values: ['Cheese', 3] }, { values: ['Peas', 5] }],
|
||||
caption: 'A shopping list',
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -131,6 +135,9 @@ describe('wagtail.contrib.typed_table_block.blocks.TypedTableBlock', () => {
|
||||
expect(document.body.innerHTML).toMatchSnapshot();
|
||||
expect(boundBlock.columns.length).toBe(2);
|
||||
expect(boundBlock.rows.length).toBe(2);
|
||||
expect(document.getElementsByName('mytable-caption')[0].value).toBe(
|
||||
'A shopping list',
|
||||
);
|
||||
});
|
||||
|
||||
test('can be cleared', () => {
|
||||
@ -141,6 +148,14 @@ describe('wagtail.contrib.typed_table_block.blocks.TypedTableBlock', () => {
|
||||
'0',
|
||||
);
|
||||
expect(document.getElementsByName('mytable-row-count')[0].value).toBe('0');
|
||||
expect(document.getElementsByName('mytable-caption')[0].value).toBe('');
|
||||
});
|
||||
|
||||
test('supports adding a caption', () => {
|
||||
boundBlock.setCaption('A shopping list');
|
||||
expect(document.getElementsByName('mytable-caption')[0].value).toBe(
|
||||
'A shopping list',
|
||||
);
|
||||
});
|
||||
|
||||
test('supports inserting columns', () => {
|
||||
|
@ -59,6 +59,7 @@ Thank you to Thibaud Colas and Badr Fourane for their work on this feature.
|
||||
* Use SlugInput on all SlugFields by default (LB (Ben) Johnston)
|
||||
* Show character counts on RichTextBlock with `max_length` (Elhussein Almasri)
|
||||
* Move locale selector in generic IndexView to a filter (Sage Abdullah)
|
||||
* Add optional caption field to `TypedTableBlock` (Tommaso Amici, Cynthia Kiser)
|
||||
|
||||
|
||||
### Bug fixes
|
||||
|
@ -76,8 +76,8 @@ class TableInputAdapter(WidgetAdapter):
|
||||
"Display the first column as a header."
|
||||
),
|
||||
"Table caption": _("Table caption"),
|
||||
"A heading that identifies the overall topic of the table, and is useful for screen reader users": _(
|
||||
"A heading that identifies the overall topic of the table, and is useful for screen reader users"
|
||||
"A heading that identifies the overall topic of the table, and is useful for screen reader users.": _(
|
||||
"A heading that identifies the overall topic of the table, and is useful for screen reader users."
|
||||
),
|
||||
"Table": _("Table"),
|
||||
}
|
||||
|
@ -40,13 +40,15 @@ class TypedTableBlockValidationError(ValidationError):
|
||||
class TypedTable:
|
||||
template = "typed_table_block/typed_table_block.html"
|
||||
|
||||
def __init__(self, columns, row_data):
|
||||
def __init__(self, columns, row_data, caption: str):
|
||||
# a list of dicts, each with items 'block' (the block instance) and 'heading'
|
||||
self.columns = columns
|
||||
|
||||
# a list of dicts, each with an item 'values' (the list of block values)
|
||||
self.row_data = row_data
|
||||
|
||||
self.caption = caption
|
||||
|
||||
@property
|
||||
def rows(self):
|
||||
"""
|
||||
@ -86,6 +88,8 @@ class BaseTypedTableBlock(Block):
|
||||
self.child_blocks[name] = block
|
||||
|
||||
def value_from_datadict(self, data, files, prefix):
|
||||
caption = data["%s-caption" % prefix]
|
||||
|
||||
column_count = int(data["%s-column-count" % prefix])
|
||||
columns = [
|
||||
{
|
||||
@ -123,6 +127,7 @@ class BaseTypedTableBlock(Block):
|
||||
{"block": col["block"], "heading": col["heading"]} for col in columns
|
||||
],
|
||||
row_data=[{"values": row["values"]} for row in rows],
|
||||
caption=caption,
|
||||
)
|
||||
|
||||
def get_prep_value(self, table):
|
||||
@ -141,11 +146,13 @@ class BaseTypedTableBlock(Block):
|
||||
}
|
||||
for row in table.row_data
|
||||
],
|
||||
"caption": table.caption,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"columns": [],
|
||||
"rows": [],
|
||||
"caption": "",
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
@ -170,11 +177,13 @@ class BaseTypedTableBlock(Block):
|
||||
{"values": [column_data[row_index] for column_data in columns_data]}
|
||||
for row_index in range(0, len(value["rows"]))
|
||||
],
|
||||
caption=value.get("caption", ""),
|
||||
)
|
||||
else:
|
||||
return TypedTable(
|
||||
columns=[],
|
||||
row_data=[],
|
||||
caption="",
|
||||
)
|
||||
|
||||
def get_form_state(self, table):
|
||||
@ -193,11 +202,13 @@ class BaseTypedTableBlock(Block):
|
||||
}
|
||||
for row in table.row_data
|
||||
],
|
||||
"caption": table.caption,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"columns": [],
|
||||
"rows": [],
|
||||
"caption": "",
|
||||
}
|
||||
|
||||
def clean(self, table):
|
||||
@ -223,10 +234,16 @@ class BaseTypedTableBlock(Block):
|
||||
if cell_errors:
|
||||
raise TypedTableBlockValidationError(cell_errors=cell_errors)
|
||||
else:
|
||||
return TypedTable(columns=table.columns, row_data=cleaned_rows)
|
||||
return TypedTable(
|
||||
columns=table.columns, row_data=cleaned_rows, caption=table.caption
|
||||
)
|
||||
|
||||
else:
|
||||
return TypedTable(columns=[], row_data=[])
|
||||
return TypedTable(
|
||||
columns=[],
|
||||
row_data=[],
|
||||
caption="",
|
||||
)
|
||||
|
||||
def deconstruct(self):
|
||||
"""
|
||||
@ -274,6 +291,10 @@ class TypedTableBlockAdapter(Adapter):
|
||||
"required": block.required,
|
||||
"icon": block.meta.icon,
|
||||
"strings": {
|
||||
"CAPTION": _("Caption"),
|
||||
"CAPTION_HELP_TEXT": _(
|
||||
"A heading that identifies the overall topic of the table, and is useful for screen reader users."
|
||||
),
|
||||
"ADD_COLUMN": _("Add column"),
|
||||
"ADD_ROW": _("Add row"),
|
||||
"COLUMN_HEADING": _("Column heading"),
|
||||
|
@ -1,5 +1,8 @@
|
||||
{% load wagtailcore_tags %}
|
||||
<table>
|
||||
{% if value.caption %}
|
||||
<caption>{{ value.caption }}</caption>
|
||||
{% endif %}
|
||||
<thead>
|
||||
<tr>
|
||||
{% for col in value.columns %}
|
||||
|
@ -38,6 +38,7 @@ class TestTableBlock(TestCase):
|
||||
)
|
||||
|
||||
self.form_data = {
|
||||
"table-caption": "Countries and their food",
|
||||
"table-column-count": "2",
|
||||
"table-row-count": "3",
|
||||
"table-column-0-type": "country",
|
||||
@ -71,6 +72,7 @@ class TestTableBlock(TestCase):
|
||||
{"values": ["nl", "A small country with stroopwafels"]},
|
||||
{"values": ["fr", "A large country with baguettes"]},
|
||||
],
|
||||
"caption": "Countries and their food",
|
||||
}
|
||||
|
||||
def test_value_from_datadict(self):
|
||||
@ -81,6 +83,7 @@ class TestTableBlock(TestCase):
|
||||
table = self.block.value_from_datadict(self.form_data, {}, "table")
|
||||
|
||||
self.assertIsInstance(table, TypedTable)
|
||||
self.assertEqual(table.caption, "Countries and their food")
|
||||
self.assertEqual(len(table.columns), 2)
|
||||
self.assertEqual(table.columns[0]["heading"], "Country")
|
||||
self.assertEqual(table.columns[1]["heading"], "Description")
|
||||
@ -103,6 +106,7 @@ class TestTableBlock(TestCase):
|
||||
# Column id 1 is a population column that was deleted before being replaced by the
|
||||
# current one with id 3.
|
||||
form_data = {
|
||||
"table-caption": "Countries and their food",
|
||||
# table-column-count includes deleted columns, as it's telling the server code
|
||||
# the maximum column ID number it should consider
|
||||
"table-column-count": "4",
|
||||
@ -129,6 +133,7 @@ class TestTableBlock(TestCase):
|
||||
table = self.block.value_from_datadict(form_data, {}, "table")
|
||||
|
||||
self.assertIsInstance(table, TypedTable)
|
||||
self.assertEqual(table.caption, "Countries and their food")
|
||||
self.assertEqual(len(table.columns), 3)
|
||||
self.assertEqual(table.columns[0]["heading"], "Country")
|
||||
self.assertEqual(table.columns[1]["heading"], "Population")
|
||||
@ -145,6 +150,7 @@ class TestTableBlock(TestCase):
|
||||
Test that we can turn JSONish data from the database into a TypedTable instance
|
||||
"""
|
||||
table = self.block.to_python(self.db_data)
|
||||
self.assertEqual(table.caption, "Countries and their food")
|
||||
self.assertIsInstance(table, TypedTable)
|
||||
self.assertEqual(len(table.columns), 2)
|
||||
self.assertEqual(table.columns[0]["heading"], "Country")
|
||||
@ -188,6 +194,7 @@ class TestTableBlock(TestCase):
|
||||
table = self.block.value_from_datadict(self.form_data, {}, "table")
|
||||
html = self.block.render(table)
|
||||
|
||||
self.assertIn("<caption>Countries and their food</caption>", html)
|
||||
self.assertIn('<th scope="col">Country</th>', html)
|
||||
# rendering should use the block renderings of the child blocks ('FR' not 'fr')
|
||||
self.assertIn("<td>FR</td>", html)
|
||||
|
Loading…
Reference in New Issue
Block a user