@dta-au/civictheme-twig 1.13.3 → 1.13.4
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/components/01-atoms/summary-list/summary-list.mdx +37 -0
- package/components/01-atoms/summary-list/summary-list.scss +107 -0
- package/components/01-atoms/summary-list/summary-list.stories.js +89 -0
- package/components/01-atoms/summary-list/summary-list.twig +47 -0
- package/components/01-atoms/table-sort/table-sort.js +199 -0
- package/components/01-atoms/table-sort/table-sort.scss +65 -0
- package/components/01-atoms/table-sort/table-sort.stories.js +142 -0
- package/components/02-molecules/feature-link-list/feature-link-list.mdx +74 -0
- package/components/02-molecules/feature-link-list/feature-link-list.scss +158 -0
- package/components/02-molecules/feature-link-list/feature-link-list.stories.data.js +40 -0
- package/components/02-molecules/feature-link-list/feature-link-list.stories.js +70 -0
- package/components/02-molecules/feature-link-list/feature-link-list.twig +106 -0
- package/components/02-molecules/group-filter/__snapshots__/group-filter.test.js.snap +10 -0
- package/components/02-molecules/group-filter/group-filter.twig +31 -0
- package/components/02-molecules/search-bar/__snapshots__/search-bar.test.js.snap +315 -0
- package/components/02-molecules/search-bar/search-bar.scss +51 -0
- package/components/02-molecules/search-bar/search-bar.stories.data.js +14 -0
- package/components/02-molecules/search-bar/search-bar.stories.js +89 -0
- package/components/02-molecules/search-bar/search-bar.test.js +92 -0
- package/components/02-molecules/search-bar/search-bar.twig +50 -0
- package/components/03-organisms/chart/chart.js +8 -2
- package/components/03-organisms/chart/chart.stories.js +1 -0
- package/components/03-organisms/chart/chart.twig +2 -0
- package/components/03-organisms/filterable-table/filterable-table.js +173 -0
- package/components/03-organisms/filterable-table/filterable-table.scss +236 -0
- package/components/03-organisms/filterable-table/filterable-table.stories.data.js +98 -0
- package/components/03-organisms/filterable-table/filterable-table.stories.js +132 -0
- package/components/03-organisms/filterable-table/filterable-table.twig +126 -0
- package/components/03-organisms/step-by-step-nav/step-by-step-nav.js +81 -0
- package/components/03-organisms/step-by-step-nav/step-by-step-nav.scss +339 -0
- package/components/03-organisms/step-by-step-nav/step-by-step-nav.stories.data.js +379 -0
- package/components/03-organisms/step-by-step-nav/step-by-step-nav.stories.js +88 -0
- package/components/03-organisms/step-by-step-nav/step-by-step-nav.twig +175 -0
- package/components/04-templates/search-results/__snapshots__/search-results.test.js.snap +3830 -0
- package/components/04-templates/search-results/search-results.js +30 -0
- package/components/04-templates/search-results/search-results.scss +370 -0
- package/components/04-templates/search-results/search-results.stories.data.js +148 -0
- package/components/04-templates/search-results/search-results.stories.js +64 -0
- package/components/04-templates/search-results/search-results.test.js +205 -0
- package/components/04-templates/search-results/search-results.twig +724 -0
- package/dist/civictheme.css +1305 -33
- package/dist/civictheme.storybook.css +1305 -33
- package/dist/civictheme.storybook.js +503 -2
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{/* Generated by packages/sdc/scripts/import-dga-dl-docs.js — re-run to refresh. */}
|
|
2
|
+
{/* Source: digital.gov.au design library (dga-dl /src/content). */}
|
|
3
|
+
{/* Cross-link rewrites deferred to beads ui-kit-dev-space-r0f — links may be broken. */}
|
|
4
|
+
|
|
5
|
+
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
|
6
|
+
import * as Stories from './summary-list.stories';
|
|
7
|
+
|
|
8
|
+
<Meta of={Stories} />
|
|
9
|
+
|
|
10
|
+
# Summary list
|
|
11
|
+
|
|
12
|
+
A structured list of name–value pairs for displaying record summaries and metadata.
|
|
13
|
+
|
|
14
|
+
<Canvas of={Stories.Default} />
|
|
15
|
+
|
|
16
|
+
Use a summary list to display structured information as labelled name–value pairs. Typical uses include review and confirm screens before form submission, metadata panels on record or profile pages, and read-only summaries of collected data.
|
|
17
|
+
|
|
18
|
+
The component renders as a styled `<dl>` element with `<dt>` (term) and `<dd>` (description) pairs.
|
|
19
|
+
|
|
20
|
+
## When to use
|
|
21
|
+
|
|
22
|
+
- data has a clear label and a corresponding value for each item
|
|
23
|
+
- summarising information a user has entered before they submit a form
|
|
24
|
+
- presenting metadata about a record, file, or entity – such as dates, statuses, or identifiers
|
|
25
|
+
- showing read-only field data on a profile or detail page
|
|
26
|
+
|
|
27
|
+
## When not to use
|
|
28
|
+
|
|
29
|
+
- for content that needs running prose – use a rich text body instead
|
|
30
|
+
- when the data is tabular with multiple columns of comparable items – use a [Table](/components/table/) instead
|
|
31
|
+
- for navigational lists – use [Manual list](/components/manual-list/) or [Feature link list](/components/feature-link-list/) instead
|
|
32
|
+
- when there are only one or two pairs – a simple paragraph is sufficient
|
|
33
|
+
|
|
34
|
+
## Related components
|
|
35
|
+
|
|
36
|
+
- [Table](/components/table/) – use when data has multiple comparable rows and columns.
|
|
37
|
+
- [Content](/components/content/) – use for free-form body text and formatted content.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//
|
|
2
|
+
// CivicTheme Summary List component styles.
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
.ct-summary-list {
|
|
6
|
+
$root: &;
|
|
7
|
+
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
|
|
11
|
+
#{$root}__row {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-wrap: wrap;
|
|
14
|
+
border-bottom: ct-particle(0.125) solid transparent;
|
|
15
|
+
padding: ct-spacing(2) 0;
|
|
16
|
+
gap: ct-spacing(1);
|
|
17
|
+
|
|
18
|
+
&:last-child {
|
|
19
|
+
border-bottom: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@include ct-breakpoint(m) {
|
|
23
|
+
flex-wrap: nowrap;
|
|
24
|
+
gap: 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&--no-border {
|
|
29
|
+
#{$root}__row {
|
|
30
|
+
border-bottom: none;
|
|
31
|
+
padding: ct-spacing(1) 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#{$root}__key {
|
|
36
|
+
@include ct-typography('label-regular');
|
|
37
|
+
|
|
38
|
+
flex: 0 0 100%;
|
|
39
|
+
margin: 0;
|
|
40
|
+
|
|
41
|
+
& > p {
|
|
42
|
+
@include ct-typography('label-regular');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@include ct-breakpoint(m) {
|
|
46
|
+
flex: 0 0 30%;
|
|
47
|
+
padding-right: ct-spacing(2);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#{$root}__value {
|
|
52
|
+
@include ct-typography('text-regular');
|
|
53
|
+
|
|
54
|
+
flex: 1;
|
|
55
|
+
margin: 0;
|
|
56
|
+
word-break: break-word;
|
|
57
|
+
overflow-wrap: anywhere;
|
|
58
|
+
|
|
59
|
+
& > p {
|
|
60
|
+
@include ct-typography('text-regular');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@include ct-breakpoint(m) {
|
|
64
|
+
flex: 1 1 auto;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#{$root}__actions {
|
|
69
|
+
margin: 0;
|
|
70
|
+
flex: 0 0 auto;
|
|
71
|
+
|
|
72
|
+
@include ct-breakpoint(m) {
|
|
73
|
+
text-align: right;
|
|
74
|
+
padding-left: ct-spacing(2);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#{$root}__action-link {
|
|
79
|
+
@include ct-typography('label-regular');
|
|
80
|
+
|
|
81
|
+
&:hover {
|
|
82
|
+
text-decoration: none;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@include ct-component-theme($root) using($root, $theme) {
|
|
87
|
+
#{$root}__row {
|
|
88
|
+
@include ct-component-property($root, $theme, row, border-color);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#{$root}__key {
|
|
92
|
+
@include ct-component-property($root, $theme, key, color);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#{$root}__value {
|
|
96
|
+
@include ct-component-property($root, $theme, value, color);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#{$root}__action-link {
|
|
100
|
+
@include ct-component-property($root, $theme, action, color);
|
|
101
|
+
|
|
102
|
+
&:hover {
|
|
103
|
+
@include ct-component-property($root, $theme, action, hover, color);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CivicTheme Summary List component stories.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Component from './summary-list.twig';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Content/Summary list',
|
|
9
|
+
component: Component,
|
|
10
|
+
tags: ['digitalgovau'],
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'padded',
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
theme: {
|
|
16
|
+
control: { type: 'radio' },
|
|
17
|
+
options: ['light', 'dark'],
|
|
18
|
+
},
|
|
19
|
+
no_border: {
|
|
20
|
+
control: { type: 'boolean' },
|
|
21
|
+
},
|
|
22
|
+
items: {
|
|
23
|
+
control: { type: 'object' },
|
|
24
|
+
},
|
|
25
|
+
modifier_class: {
|
|
26
|
+
control: { type: 'text' },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
|
|
33
|
+
const defaultItems = [
|
|
34
|
+
{ key: 'Full name', value: 'Jane Citizen' },
|
|
35
|
+
{ key: 'Date of birth', value: '1 January 1980' },
|
|
36
|
+
{ key: 'Contact email', value: 'jane.citizen@example.gov.au' },
|
|
37
|
+
{ key: 'Address', value: '123 Example Street, Canberra ACT 2600' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const Default = {
|
|
41
|
+
name: 'Light theme',
|
|
42
|
+
args: {
|
|
43
|
+
theme: 'light',
|
|
44
|
+
items: defaultItems,
|
|
45
|
+
no_border: false,
|
|
46
|
+
modifier_class: '',
|
|
47
|
+
attributes: null,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const DarkTheme = {
|
|
52
|
+
name: 'Dark theme',
|
|
53
|
+
globals: {
|
|
54
|
+
backgrounds: { value: 'dark' },
|
|
55
|
+
},
|
|
56
|
+
args: {
|
|
57
|
+
theme: 'dark',
|
|
58
|
+
items: defaultItems.slice(0, 3),
|
|
59
|
+
no_border: false,
|
|
60
|
+
modifier_class: '',
|
|
61
|
+
attributes: null,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const NoBorder = {
|
|
66
|
+
name: 'No border',
|
|
67
|
+
args: {
|
|
68
|
+
theme: 'light',
|
|
69
|
+
items: defaultItems.slice(0, 3),
|
|
70
|
+
no_border: true,
|
|
71
|
+
modifier_class: '',
|
|
72
|
+
attributes: null,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const WithActions = {
|
|
77
|
+
name: 'With actions',
|
|
78
|
+
args: {
|
|
79
|
+
theme: 'light',
|
|
80
|
+
no_border: false,
|
|
81
|
+
modifier_class: '',
|
|
82
|
+
attributes: null,
|
|
83
|
+
items: [
|
|
84
|
+
{ key: 'Full name', value: 'Jane Citizen', action_url: '#', action_text: 'Change' },
|
|
85
|
+
{ key: 'Date of birth', value: '1 January 1980', action_url: '#', action_text: 'Change' },
|
|
86
|
+
{ key: 'Contact email', value: 'jane.citizen@example.gov.au', action_url: '#', action_text: 'Change' },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{#
|
|
2
|
+
/**
|
|
3
|
+
* @file
|
|
4
|
+
* CivicTheme Summary List component.
|
|
5
|
+
*
|
|
6
|
+
* Props:
|
|
7
|
+
* - theme: [string]
|
|
8
|
+
* - no_border: [boolean]
|
|
9
|
+
* - items: [array] :
|
|
10
|
+
* Each item contains:
|
|
11
|
+
* - key: [string]
|
|
12
|
+
* - value: [string]
|
|
13
|
+
* - action_url: [string]
|
|
14
|
+
* - action_text: [string]
|
|
15
|
+
* - modifier_class: [string]
|
|
16
|
+
* - attributes: [Drupal\Core\Template\Attribute] Additional HTML attributes.
|
|
17
|
+
*/
|
|
18
|
+
#}
|
|
19
|
+
|
|
20
|
+
{% set theme = theme in ['light', 'dark'] ? theme : 'light' %}
|
|
21
|
+
{% set theme_class = 'ct-theme-%s'|format(theme) %}
|
|
22
|
+
{% set no_border_class = no_border ? 'ct-summary-list--no-border' : '' %}
|
|
23
|
+
{% set modifier_class = '%s %s %s'|format(theme_class, no_border_class, modifier_class|default('')) %}
|
|
24
|
+
|
|
25
|
+
{% if items is not empty %}
|
|
26
|
+
<dl
|
|
27
|
+
class="ct-summary-list {{ modifier_class|trim }}"
|
|
28
|
+
data-component-name="ct-summary-list"
|
|
29
|
+
{%- if attributes is defined and attributes is not null %} {{- attributes -}}{% endif %}
|
|
30
|
+
>
|
|
31
|
+
{% for item in items %}
|
|
32
|
+
{% if item.key is not empty and item.value is not empty %}
|
|
33
|
+
<div class="ct-summary-list__row">
|
|
34
|
+
<dt class="ct-summary-list__key">{{ item.key }}</dt>
|
|
35
|
+
<dd class="ct-summary-list__value">{{ item.value }}</dd>
|
|
36
|
+
{% if item.action_url is not empty %}
|
|
37
|
+
<dd class="ct-summary-list__actions">
|
|
38
|
+
<a href="{{ item.action_url }}" class="ct-summary-list__action-link">
|
|
39
|
+
{{- item.action_text|default('Change') -}}
|
|
40
|
+
</a>
|
|
41
|
+
</dd>
|
|
42
|
+
{% endif %}
|
|
43
|
+
</div>
|
|
44
|
+
{% endif %}
|
|
45
|
+
{% endfor %}
|
|
46
|
+
</dl>
|
|
47
|
+
{% endif %}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* Table Sort — framework-agnostic progressive enhancement.
|
|
4
|
+
*
|
|
5
|
+
* Finds all tables with the class `ct-table--sortable` and progressively
|
|
6
|
+
* enhances them with MOJ-style sortable column header buttons.
|
|
7
|
+
*
|
|
8
|
+
* Usage: apply the class `ct-table--sortable` to any <table> element. The
|
|
9
|
+
* behavior attaches automatically via DOMContentLoaded (static/Storybook) or
|
|
10
|
+
* Drupal.behaviors (Drupal). Stories call window.DgaTableSort.initAll().
|
|
11
|
+
*
|
|
12
|
+
* Pre-sorted columns: add aria-sort="ascending|descending" directly to the
|
|
13
|
+
* <th>, or add the class `ct-sort--asc` / `ct-sort--desc`.
|
|
14
|
+
*
|
|
15
|
+
* Custom sort values: add data-sort-value="..." to any <td> to override what
|
|
16
|
+
* value is used for sorting (e.g. a numeric timestamp for a date cell).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/* global Drupal */
|
|
20
|
+
|
|
21
|
+
(function () {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
// SVG arrow icons — MOJ Design System pattern.
|
|
25
|
+
// focusable="false" + aria-hidden="true" hides icons from assistive tech.
|
|
26
|
+
// fill="currentColor" ensures visibility in Windows High Contrast Mode.
|
|
27
|
+
const SVG_UP = '<svg width="22" height="22" focusable="false" aria-hidden="true" role="img" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.5625 15.5L11 6.63125L15.4375 15.5H6.5625Z" fill="currentColor"/></svg>';
|
|
28
|
+
const SVG_DOWN = '<svg width="22" height="22" focusable="false" aria-hidden="true" role="img" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.4375 7L11 15.8687L6.5625 7L15.4375 7Z" fill="currentColor"/></svg>';
|
|
29
|
+
const SVG_UPDOWN = '<svg width="22" height="22" focusable="false" aria-hidden="true" role="img" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.1875 9.5L10.9609 3.95703L13.7344 9.5H8.1875Z" fill="currentColor"/><path d="M13.7344 12.0781L10.9609 17.6211L8.1875 12.0781H13.7344Z" fill="currentColor"/></svg>';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function getCellValue(cell) {
|
|
36
|
+
if (!cell) return '';
|
|
37
|
+
const raw = cell.getAttribute('data-sort-value') || cell.textContent.trim();
|
|
38
|
+
const num = Number(raw);
|
|
39
|
+
return Number.isFinite(num) ? num : raw;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function updateIcons(headings) {
|
|
43
|
+
headings.forEach((th) => {
|
|
44
|
+
const btn = th.querySelector('.ct-sort-btn');
|
|
45
|
+
if (!btn) return;
|
|
46
|
+
|
|
47
|
+
const existing = btn.querySelector('svg');
|
|
48
|
+
if (existing) existing.remove();
|
|
49
|
+
|
|
50
|
+
const dir = th.getAttribute('aria-sort');
|
|
51
|
+
btn.insertAdjacentHTML('beforeend',
|
|
52
|
+
dir === 'ascending' ? SVG_UP :
|
|
53
|
+
dir === 'descending' ? SVG_DOWN :
|
|
54
|
+
SVG_UPDOWN
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sortRows(tbody, colIndex, direction) {
|
|
60
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
61
|
+
const ascending = direction === 'ascending';
|
|
62
|
+
|
|
63
|
+
rows.sort((a, b) => {
|
|
64
|
+
const aCell = a.querySelectorAll('td, th')[colIndex];
|
|
65
|
+
const bCell = b.querySelectorAll('td, th')[colIndex];
|
|
66
|
+
const aVal = getCellValue(aCell);
|
|
67
|
+
const bVal = getCellValue(bCell);
|
|
68
|
+
|
|
69
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
70
|
+
return ascending ? aVal - bVal : bVal - aVal;
|
|
71
|
+
}
|
|
72
|
+
return ascending
|
|
73
|
+
? String(aVal).localeCompare(String(bVal))
|
|
74
|
+
: String(bVal).localeCompare(String(aVal));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
rows.forEach((row) => tbody.appendChild(row));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function initialiseSortedColumn(headings, tbody) {
|
|
81
|
+
headings.forEach((th) => {
|
|
82
|
+
const dir = th.getAttribute('aria-sort');
|
|
83
|
+
if (dir !== 'ascending' && dir !== 'descending') return;
|
|
84
|
+
const btn = th.querySelector('.ct-sort-btn');
|
|
85
|
+
if (!btn) return;
|
|
86
|
+
sortRows(tbody, parseInt(btn.getAttribute('data-index'), 10), dir);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Core enhancement — no Drupal dependency.
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
function initTable(table) {
|
|
95
|
+
// Idempotency guard — replaces once().
|
|
96
|
+
if (table.dataset.tableSortInit) return;
|
|
97
|
+
table.dataset.tableSortInit = 'true';
|
|
98
|
+
|
|
99
|
+
const thead = table.querySelector('thead');
|
|
100
|
+
const tbody = table.querySelector('tbody');
|
|
101
|
+
|
|
102
|
+
if (!thead || !tbody) return;
|
|
103
|
+
|
|
104
|
+
const headings = Array.from(thead.querySelectorAll('th'));
|
|
105
|
+
|
|
106
|
+
// Resolve initial aria-sort state for every heading.
|
|
107
|
+
// Priority: existing aria-sort attribute > ct-sort--* class > 'none'.
|
|
108
|
+
headings.forEach((th) => {
|
|
109
|
+
const preset = th.getAttribute('aria-sort')
|
|
110
|
+
|| (th.classList.contains('ct-sort--asc') ? 'ascending'
|
|
111
|
+
: th.classList.contains('ct-sort--desc') ? 'descending'
|
|
112
|
+
: 'none');
|
|
113
|
+
th.setAttribute('aria-sort', preset);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Inject a sort button into every heading.
|
|
117
|
+
headings.forEach((th, index) => {
|
|
118
|
+
if (th.querySelector('.ct-sort-btn')) return;
|
|
119
|
+
|
|
120
|
+
const label = th.textContent.trim();
|
|
121
|
+
const btn = document.createElement('button');
|
|
122
|
+
btn.type = 'button';
|
|
123
|
+
btn.className = 'ct-sort-btn';
|
|
124
|
+
btn.setAttribute('data-index', String(index));
|
|
125
|
+
btn.textContent = label;
|
|
126
|
+
|
|
127
|
+
th.textContent = '';
|
|
128
|
+
th.appendChild(btn);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
updateIcons(headings);
|
|
132
|
+
initialiseSortedColumn(headings, tbody);
|
|
133
|
+
|
|
134
|
+
// aria-live region so screen readers announce sort changes.
|
|
135
|
+
const status = document.createElement('div');
|
|
136
|
+
status.setAttribute('aria-atomic', 'true');
|
|
137
|
+
status.setAttribute('aria-live', 'polite');
|
|
138
|
+
status.setAttribute('role', 'status');
|
|
139
|
+
status.className = 'ct-visually-hidden';
|
|
140
|
+
table.insertAdjacentElement('afterend', status);
|
|
141
|
+
|
|
142
|
+
// Abort any previous listener — prevents accumulation when the idempotency
|
|
143
|
+
// guard is cleared on HMR reuse.
|
|
144
|
+
if (table.sortAbort) table.sortAbort.abort();
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
table.sortAbort = controller;
|
|
147
|
+
|
|
148
|
+
// Single delegated listener on thead — avoids per-button listeners.
|
|
149
|
+
thead.addEventListener('click', (event) => {
|
|
150
|
+
const btn = event.target.closest('.ct-sort-btn');
|
|
151
|
+
if (!btn) return;
|
|
152
|
+
|
|
153
|
+
const th = btn.parentElement;
|
|
154
|
+
const current = th.getAttribute('aria-sort');
|
|
155
|
+
const newDir = (current === 'none' || current === 'descending') ? 'ascending' : 'descending';
|
|
156
|
+
const colIndex = parseInt(btn.getAttribute('data-index'), 10);
|
|
157
|
+
|
|
158
|
+
headings.forEach((h) => h.setAttribute('aria-sort', 'none'));
|
|
159
|
+
th.setAttribute('aria-sort', newDir);
|
|
160
|
+
|
|
161
|
+
updateIcons(headings);
|
|
162
|
+
sortRows(tbody, colIndex, newDir);
|
|
163
|
+
|
|
164
|
+
status.textContent = `Sort by ${btn.textContent.trim()} (${newDir})`;
|
|
165
|
+
}, { signal: controller.signal });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Initialise all sortable tables within a given context element.
|
|
170
|
+
*
|
|
171
|
+
* @param {Element|Document} [context=document]
|
|
172
|
+
*/
|
|
173
|
+
function initAll(context) {
|
|
174
|
+
(context || document).querySelectorAll('.ct-table--sortable').forEach(initTable);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Integration.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
// Expose globally for Storybook play() and static pages.
|
|
182
|
+
window.DgaTableSort = { initAll };
|
|
183
|
+
|
|
184
|
+
// Drupal: register as a behavior for AJAX-safe re-attachment.
|
|
185
|
+
if (typeof Drupal !== 'undefined') {
|
|
186
|
+
Drupal.behaviors.dgaTableSort = {
|
|
187
|
+
attach(context) {
|
|
188
|
+
initAll(context);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
} else {
|
|
192
|
+
// Non-Drupal: init elements in the DOM now, then watch for future
|
|
193
|
+
// insertions via MutationObserver (covers static pages and Storybook).
|
|
194
|
+
initAll();
|
|
195
|
+
new MutationObserver(() => initAll())
|
|
196
|
+
.observe(document.body || document.documentElement, { childList: true, subtree: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
})();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Sort button styles. Applied when the DgaTableSort behavior enhances a `.ct-table--sortable` table.
|
|
2
|
+
// Buttons are injected into <th> cells by JS — styles are global-scoped since
|
|
3
|
+
// we're enhancing existing CivicTheme tables, not a component-owned element.
|
|
4
|
+
|
|
5
|
+
.ct-basic-content table,
|
|
6
|
+
.table-container table,
|
|
7
|
+
.ct-table {
|
|
8
|
+
th {
|
|
9
|
+
&:has(> .ct-sort-btn) {
|
|
10
|
+
padding: ct-spacing(1.5) ct-spacing(3);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.ct-sort-btn {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: ct-spacing(1);
|
|
19
|
+
width: 100%;
|
|
20
|
+
padding: ct-spacing(1) 0;
|
|
21
|
+
background: none;
|
|
22
|
+
border: none;
|
|
23
|
+
font-family: inherit;
|
|
24
|
+
font-size: inherit;
|
|
25
|
+
font-weight: inherit;
|
|
26
|
+
color: inherit;
|
|
27
|
+
text-align: left;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
|
|
31
|
+
&:focus-visible {
|
|
32
|
+
@include ct-outline();
|
|
33
|
+
@include ct-outline-border();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// SVG sort indicator — injected by JS, sized to scale with text.
|
|
37
|
+
svg {
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
margin-left: auto;
|
|
40
|
+
|
|
41
|
+
// Dim the up/down icon when the column is unsorted.
|
|
42
|
+
[aria-sort='none'] & {
|
|
43
|
+
opacity: 0.4;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// .ct-sort-btn is injected as a child of <th> and is never the themed root,
|
|
49
|
+
// so ct-component-theme() (which targets &.ct-theme-*) cannot be used here.
|
|
50
|
+
// The parent selector is the correct pattern for descendant-only theming.
|
|
51
|
+
.ct-theme-dark .ct-sort-btn:focus-visible {
|
|
52
|
+
@include ct-outline(true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Screen-reader-only utility for the aria-live status region.
|
|
56
|
+
.ct-visually-hidden {
|
|
57
|
+
position: absolute;
|
|
58
|
+
width: 1px;
|
|
59
|
+
height: 1px;
|
|
60
|
+
margin: 0;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
clip: rect(0 0 0 0);
|
|
63
|
+
clip-path: inset(50%);
|
|
64
|
+
white-space: nowrap;
|
|
65
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CivicTheme Table Sort atom stories.
|
|
3
|
+
*
|
|
4
|
+
* Table Sort is a pure CSS/JS enhancement — there is no Twig template.
|
|
5
|
+
* Apply the class `ct-table--sortable` to any <table> element to enable
|
|
6
|
+
* MOJ-style sortable column header buttons.
|
|
7
|
+
*
|
|
8
|
+
* Pre-sorted columns: add aria-sort="ascending|descending" to a <th>.
|
|
9
|
+
* Custom sort values: add data-sort-value="..." to a <td>.
|
|
10
|
+
*
|
|
11
|
+
* CSS and JS are imported explicitly here because the sdc-plugin auto-discovers
|
|
12
|
+
* assets via Twig imports — this component has no Twig template, so they must
|
|
13
|
+
* be declared manually.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import './table-sort.css';
|
|
17
|
+
import './table-sort.js';
|
|
18
|
+
// table.js sets data-title attrs on tbody cells so mobile pseudo-element labels
|
|
19
|
+
// render; table.css carries the matching `content: attr(data-title)` rule. The
|
|
20
|
+
// sdc-plugin only auto-loads sibling assets via Twig imports — this atom has no
|
|
21
|
+
// Twig template, so the table assets must be pulled in explicitly.
|
|
22
|
+
import '../table/table.js';
|
|
23
|
+
import '../table/table.css';
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
title: 'Content/Tables/Table sort',
|
|
27
|
+
tags: ['digitalgovau'],
|
|
28
|
+
parameters: {
|
|
29
|
+
layout: 'padded',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
|
|
35
|
+
function teardownTableSort(canvasElement) {
|
|
36
|
+
canvasElement.querySelectorAll('.ct-table--sortable').forEach((el) => {
|
|
37
|
+
if (!el.dataset.tableSortInit) return;
|
|
38
|
+
const next = el.nextElementSibling;
|
|
39
|
+
if (next?.getAttribute('role') === 'status') next.remove();
|
|
40
|
+
delete el.dataset.tableSortInit;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const TableSort = {
|
|
45
|
+
play: async ({ canvasElement }) => {
|
|
46
|
+
teardownTableSort(canvasElement);
|
|
47
|
+
window.DgaTableSort.initAll(canvasElement);
|
|
48
|
+
},
|
|
49
|
+
render: () => `
|
|
50
|
+
<div class="container">
|
|
51
|
+
<div class="row">
|
|
52
|
+
<div class="col-xxs-12">
|
|
53
|
+
|
|
54
|
+
<table class="ct-table ct-table--sortable ct-theme-light">
|
|
55
|
+
<thead>
|
|
56
|
+
<tr>
|
|
57
|
+
<th>Entity</th>
|
|
58
|
+
<th>Portfolio</th>
|
|
59
|
+
<th aria-sort="ascending">Year</th>
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody>
|
|
63
|
+
<tr>
|
|
64
|
+
<td>Australian Taxation Office</td>
|
|
65
|
+
<td>Treasury</td>
|
|
66
|
+
<td data-sort-value="2024">2024</td>
|
|
67
|
+
</tr>
|
|
68
|
+
<tr>
|
|
69
|
+
<td>Department of Finance</td>
|
|
70
|
+
<td>Finance</td>
|
|
71
|
+
<td data-sort-value="2023">2023</td>
|
|
72
|
+
</tr>
|
|
73
|
+
<tr>
|
|
74
|
+
<td>Services Australia</td>
|
|
75
|
+
<td>Finance</td>
|
|
76
|
+
<td data-sort-value="2025">2025</td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr>
|
|
79
|
+
<td>Australian Federal Police</td>
|
|
80
|
+
<td>Attorney General's</td>
|
|
81
|
+
<td data-sort-value="2022">2022</td>
|
|
82
|
+
</tr>
|
|
83
|
+
<tr>
|
|
84
|
+
<td>Digital Transformation Agency</td>
|
|
85
|
+
<td>Finance</td>
|
|
86
|
+
<td data-sort-value="2024">2024</td>
|
|
87
|
+
</tr>
|
|
88
|
+
</tbody>
|
|
89
|
+
</table>
|
|
90
|
+
</div></div></div>
|
|
91
|
+
`,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const TableSortWithPreSortedColumn = {
|
|
95
|
+
name: 'Pre-sorted Column (descending)',
|
|
96
|
+
play: async ({ canvasElement }) => {
|
|
97
|
+
teardownTableSort(canvasElement);
|
|
98
|
+
window.DgaTableSort.initAll(canvasElement);
|
|
99
|
+
},
|
|
100
|
+
render: () => `
|
|
101
|
+
<div class="container">
|
|
102
|
+
<div class="row">
|
|
103
|
+
<div class="col-xxs-12">
|
|
104
|
+
<table class="ct-table ct-table--sortable ct-theme-light">
|
|
105
|
+
<thead>
|
|
106
|
+
<tr>
|
|
107
|
+
<th aria-sort="descending">Entity</th>
|
|
108
|
+
<th>Portfolio</th>
|
|
109
|
+
<th>Year</th>
|
|
110
|
+
</tr>
|
|
111
|
+
</thead>
|
|
112
|
+
<tbody>
|
|
113
|
+
<tr>
|
|
114
|
+
<td>Australian Taxation Office</td>
|
|
115
|
+
<td>Treasury</td>
|
|
116
|
+
<td data-sort-value="2024">2024</td>
|
|
117
|
+
</tr>
|
|
118
|
+
<tr>
|
|
119
|
+
<td>Department of Finance</td>
|
|
120
|
+
<td>Finance</td>
|
|
121
|
+
<td data-sort-value="2023">2023</td>
|
|
122
|
+
</tr>
|
|
123
|
+
<tr>
|
|
124
|
+
<td>Services Australia</td>
|
|
125
|
+
<td>Finance</td>
|
|
126
|
+
<td data-sort-value="2025">2025</td>
|
|
127
|
+
</tr>
|
|
128
|
+
<tr>
|
|
129
|
+
<td>Australian Federal Police</td>
|
|
130
|
+
<td>Attorney General's</td>
|
|
131
|
+
<td data-sort-value="2022">2022</td>
|
|
132
|
+
</tr>
|
|
133
|
+
<tr>
|
|
134
|
+
<td>Digital Transformation Agency</td>
|
|
135
|
+
<td>Finance</td>
|
|
136
|
+
<td data-sort-value="2024">2024</td>
|
|
137
|
+
</tr>
|
|
138
|
+
</tbody>
|
|
139
|
+
</table>
|
|
140
|
+
</div></div></div>
|
|
141
|
+
`,
|
|
142
|
+
};
|