@ons/design-system 72.10.0 → 72.10.1

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.
Files changed (32) hide show
  1. package/components/char-check-limit/_macro.njk +2 -2
  2. package/components/char-check-limit/character-check.js +30 -9
  3. package/components/char-check-limit/character-check.spec.js +1 -1
  4. package/components/chart/_chart.scss +2 -3
  5. package/components/chart/_macro.njk +27 -16
  6. package/components/chart/_macro.spec.js +19 -0
  7. package/components/chart/bar-chart.js +46 -20
  8. package/components/chart/chart-constants.js +1 -0
  9. package/components/chart/chart.js +17 -1
  10. package/components/chart/common-chart-options.js +57 -28
  11. package/components/chart/example-column-chart-with-custom-reference-line-value.njk +56 -0
  12. package/components/chart/example-line-chart-with-custom-reference-line-value.njk +224 -0
  13. package/components/chart/example-line-chart-with-markers.njk +20 -20
  14. package/components/chart/example-scatter-chart.njk +4 -4
  15. package/components/chart/line-chart.js +29 -11
  16. package/components/chart/range-annotations-options.js +6 -1
  17. package/components/chart/scatter-chart.js +0 -6
  18. package/components/chart/specific-chart-options.js +21 -0
  19. package/components/chart/utilities.js +8 -7
  20. package/components/checkboxes/_macro.spec.js +1 -1
  21. package/components/mutually-exclusive/mutually-exclusive.textarea.spec.js +1 -1
  22. package/components/textarea/_macro.njk +8 -6
  23. package/components/textarea/_macro.spec.js +12 -8
  24. package/components/textarea/{example-textarea-with-character-limit.njk → example-textarea-with-character-check.njk} +3 -1
  25. package/css/main.css +1 -1
  26. package/js/main.js +0 -1
  27. package/package.json +14 -14
  28. package/scripts/main.es5.js +1 -1
  29. package/scripts/main.js +1 -1
  30. package/components/char-check-limit/character-limit.js +0 -55
  31. package/components/textarea/textarea.dom.js +0 -12
  32. package/components/textarea/textarea.spec.js +0 -98
@@ -8,8 +8,8 @@
8
8
  class="ons-input__limit ons-u-fs-s--b ons-u-d-no ons-u-mt-2xs"
9
9
  data-charcount-singular="{{ params.charCountSingular }}"
10
10
  data-charcount-plural="{{ params.charCountPlural }}"
11
- data-charcount-limit-singular="{{ params.charCountOverLimitSingular }}"
12
- data-charcount-limit-plural="{{ params.charCountOverLimitPlural }}"
11
+ data-charcount-limit-singular="{{ params.charCountOverLimitSingular | default("You have exceeded the character limit by {x} character") }}"
12
+ data-charcount-limit-plural="{{ params.charCountOverLimitPlural | default("You have exceeded the character limit by {x} characters") }}"
13
13
  >
14
14
  </span>
15
15
  {% endmacro %}
@@ -6,9 +6,18 @@ const attrCharCheckVal = 'data-char-check-num';
6
6
 
7
7
  export default class CharCheck {
8
8
  constructor(context) {
9
- this.context = context;
10
- this.input = this.context.querySelector('input');
11
- this.button = this.context.parentNode.querySelector('button');
9
+ this.tagName = context.tagName;
10
+
11
+ // Handle either an input directly or a container with an input inside
12
+ if (this.tagName.toLowerCase() === 'input' || this.tagName.toLowerCase() === 'textarea') {
13
+ this.input = context;
14
+ } else {
15
+ this.input = context.querySelector('input');
16
+ }
17
+
18
+ // Find the button: if input is passed directly, look at its parent
19
+ let parent = this.input.parentNode;
20
+ this.button = parent ? parent.querySelector('button') : null;
12
21
  this.checkElement = document.getElementById(this.input.getAttribute(attrCharCheckRef));
13
22
  this.checkVal = this.input.getAttribute(attrCharCheckVal);
14
23
  this.countdown = this.input.getAttribute(attrCharCheckCountdown) || false;
@@ -22,15 +31,17 @@ export default class CharCheck {
22
31
  if (this.button) {
23
32
  this.setButtonState(this.checkVal);
24
33
  }
34
+
25
35
  this.input.addEventListener('input', this.updateCheckReadout.bind(this));
26
36
  }
27
37
 
28
38
  updateCheckReadout(event, firstRun) {
29
39
  const value = this.input.value;
30
- const remaining = this.checkVal - value.length;
40
+ const remaining = this.checkVal - this.getCharLength(value);
41
+
31
42
  // Prevent aria live announcement when component initialises
32
43
  if (!firstRun && event.inputType) {
33
- this.checkElement.setAttribute('aria-live', 'polite');
44
+ this.checkElement.setAttribute('aria-live', [remaining > 0 ? 'polite' : 'assertive']);
34
45
  } else {
35
46
  this.checkElement.removeAttribute('aria-live');
36
47
  }
@@ -67,13 +78,23 @@ export default class CharCheck {
67
78
  }
68
79
 
69
80
  setShowMessage(remaining) {
70
- this.checkElement.classList[(remaining < this.checkVal && remaining > 0 && this.countdown) || remaining < 0 ? 'remove' : 'add'](
71
- 'ons-u-d-no',
72
- );
81
+ if (this.tagName.toLowerCase() === 'textarea') {
82
+ // Always display the remaining character message for textarea
83
+ this.checkElement.classList['remove']('ons-u-d-no');
84
+ } else {
85
+ this.checkElement.classList[(remaining < this.checkVal && remaining > 0 && this.countdown) || remaining < 0 ? 'remove' : 'add'](
86
+ 'ons-u-d-no',
87
+ );
88
+ }
73
89
  }
74
90
 
75
91
  setCheckClass(remaining, element, setClass) {
76
92
  element.classList[remaining < 0 ? 'add' : 'remove'](setClass);
77
- this.checkElement.setAttribute('aria-live', [remaining > 0 ? 'polite' : 'assertive']);
93
+ }
94
+
95
+ getCharLength(text) {
96
+ // line breaks count as two characters as forms convert to \n\r when submitted
97
+ const lineBreaks = (text.match(/\n/g) || []).length;
98
+ return text.length + lineBreaks;
78
99
  }
79
100
  }
@@ -192,7 +192,7 @@ describe('script: character-check', () => {
192
192
  });
193
193
 
194
194
  it('then aria-live attribute should removed', async () => {
195
- const hasAriaLiveAttribute = await page.$eval('#feedback-lim', (element) => element.hasAttribute('aria-live'));
195
+ const hasAriaLiveAttribute = await page.$eval('#feedback-check', (element) => element.hasAttribute('aria-live'));
196
196
  expect(hasAriaLiveAttribute).toBe(false);
197
197
  });
198
198
  });
@@ -91,15 +91,14 @@
91
91
 
92
92
  &-item {
93
93
  &--uncertainty {
94
- width: 12px;
95
- height: 12px;
94
+ width: 14px;
95
+ height: 14px;
96
96
  background-color: rgb(32 96 149 / 65%);
97
97
  }
98
98
 
99
99
  &--estimate {
100
100
  width: 20px;
101
101
  height: 3px;
102
- border-radius: 2px;
103
102
  background-color: #003c57;
104
103
  }
105
104
  }
@@ -47,6 +47,9 @@
47
47
  {% if params.uncertaintyRangeLabel %}
48
48
  data-highcharts-uncertainty-range-label="{{ params.uncertaintyRangeLabel }}"
49
49
  {% endif %}
50
+ {% if params.yAxis.customReferenceLineValue %}
51
+ data-highcharts-custom-reference-line-value="{{ params.yAxis.customReferenceLineValue }}"
52
+ {% endif %}
50
53
  >
51
54
  <figure class="ons-chart">
52
55
  {{ openingTitleTag | safe }} class="ons-chart__title">{{ params.title }}{{ closingTitleTag | safe }}
@@ -119,23 +122,31 @@
119
122
  {% endif %}
120
123
  {% if params.chartType in supportedChartTypes %}
121
124
  {% set series = [] %}
125
+ {% set lineSeriesCount = 0 %}
122
126
  {% for item in params.series %}
123
- {%
124
- set seriesItem = {
125
- "name": item.name if item.name else '',
126
- "data": item.data if item.data else [],
127
- "marker": {
128
- "enabled": item.marker if item.marker else false
129
- },
130
- "dataLabels": {
131
- "enabled": item.dataLabels if item.dataLabels else false
132
- },
133
- "connectNulls": item.connectNulls if item.connectNulls else false,
134
- "type": item.type if item.type and (item.type == 'line' and params.chartType in supportedChartTypesWithLine) or (item.type == 'scatter' and params.chartType in supportedChartTypesWithScatter) else params.chartType
135
- }
136
- %}
137
- {# Use `set` tag to avoid printing the return value of extend #}
138
- {% set _ = extend(series, seriesItem) %}
127
+ {% if item.type == 'line' and lineSeriesCount > 0 %}
128
+ {# skip extra line series as the DS guidelines only allow for one extra line series #}
129
+ {% else %}
130
+ {%
131
+ set seriesItem = {
132
+ "name": item.name if item.name else '',
133
+ "data": item.data if item.data else [],
134
+ "marker": {
135
+ "enabled": item.marker if item.marker else false
136
+ },
137
+ "dataLabels": {
138
+ "enabled": item.dataLabels if item.dataLabels else false
139
+ },
140
+ "connectNulls": item.connectNulls if item.connectNulls else false,
141
+ "type": item.type if item.type and (item.type == 'line' and params.chartType in supportedChartTypesWithLine) or (item.type == 'scatter' and params.chartType in supportedChartTypesWithScatter) else params.chartType
142
+ }
143
+ %}
144
+ {# Use `set` tag to avoid printing the return value of extend #}
145
+ {% set _ = extend(series, seriesItem) %}
146
+ {% endif %}
147
+ {% if item.type == 'line' %}
148
+ {% set lineSeriesCount = lineSeriesCount + 1 %}
149
+ {% endif %}
139
150
  {% endfor %}
140
151
  {# Set the legend value to true by default #}
141
152
  {% set legendValue = true %}
@@ -702,6 +702,7 @@ describe('Macro: Chart', () => {
702
702
  expect($('[data-highcharts-base-chart]').attr('data-highcharts-theme')).toBe('alternate');
703
703
  expect($('[data-highcharts-base-chart]').attr('data-highcharts-title')).toBe('Example Column Chart');
704
704
  expect($('[data-highcharts-base-chart]').attr('data-highcharts-id')).toBe('column-chart-123');
705
+ expect($('[data-highcharts-base-chart]').attr('data-highcharts-custom-reference-line-value')).toBe('10');
705
706
  });
706
707
 
707
708
  test('THEN: it includes the Highcharts JSON config', () => {
@@ -709,6 +710,24 @@ describe('Macro: Chart', () => {
709
710
  expect(configScript).toContain('"text":"Y Axis Title"');
710
711
  });
711
712
  });
713
+ describe('WHEN: more than one line is provided', () => {
714
+ const $ = cheerio.load(
715
+ renderComponent('chart', {
716
+ ...EXAMPLE_COLUMN_WITH_LINE_CHART_PARAMS,
717
+ series: [
718
+ ...EXAMPLE_COLUMN_WITH_LINE_CHART_PARAMS.series,
719
+ { name: 'Additional Line', data: [15, 25, 35], type: 'line' },
720
+ { name: 'Another additional Line', data: [14, 27, 31], type: 'line' },
721
+ ],
722
+ }),
723
+ );
724
+ const configScript = $(`script[data-highcharts-config--column-chart-123]`).html();
725
+
726
+ test('THEN: it does not include the additional line series', () => {
727
+ const lineTypeMatches = (configScript.match(/"type":"line"/g) || []).length;
728
+ expect(lineTypeMatches).toBe(1);
729
+ });
730
+ });
712
731
  });
713
732
 
714
733
  describe('GIVEN: Params: Legend', () => {
@@ -70,29 +70,55 @@ class BarChart {
70
70
  };
71
71
 
72
72
  // Updates the config to move the data labels inside the bars, but only if the bar is wide enough
73
- // This may also need to run when the chart is resized
73
+ // This also runs when the chart is resized
74
74
  postLoadDataLabels = (currentChart) => {
75
- const insideOptions = {
76
- dataLabels: this.getBarChartLabelsInsideOptions(),
77
- };
78
- const outsideOptions = {
79
- dataLabels: this.getBarChartLabelsOutsideOptions(),
80
- };
81
-
75
+ const insideOptions = this.getBarChartLabelsInsideOptions();
76
+ const outsideOptions = this.getBarChartLabelsOutsideOptions();
82
77
  currentChart.series.forEach((series) => {
78
+ // If we have a bar chart with an extra line, exit early for the line series
79
+ if (series.type == 'line') {
80
+ return;
81
+ }
82
+
83
+ if (series.type == 'scatter') {
84
+ // If we have a bar chart with confidence levels, exit early for the scatter series
85
+ return;
86
+ }
87
+
83
88
  const points = series.data;
89
+
84
90
  points.forEach((point) => {
85
- // Get the actual width of the data label
86
- const labelWidth = point.dataLabel && point.dataLabel.getBBox().width;
87
- // Move the data labels inside the bar if the bar is wider than the label plus some padding
88
- if (series.type == 'scatter') {
89
- // If we have a bar chart with confidence levels, exit early for the scatter series
90
- return;
91
- }
92
- if (point.shapeArgs.height > labelWidth + 20) {
93
- point.update(insideOptions, false);
94
- } else {
95
- point.update(outsideOptions, false);
91
+ if (point.dataLabel) {
92
+ // Get the actual width of the data label
93
+ const labelWidth = point.dataLabel.getBBox().width;
94
+
95
+ // Move the data labels inside the bar if the bar is wider than the label plus some padding
96
+ if (point.shapeArgs.height > labelWidth + 5) {
97
+ // Negative values are aligned on the left, positive values on the right
98
+ if (point.y < 0) {
99
+ point.update(
100
+ {
101
+ dataLabels: {
102
+ ...insideOptions,
103
+ align: 'left',
104
+ },
105
+ },
106
+ false,
107
+ );
108
+ } else {
109
+ point.update(
110
+ {
111
+ dataLabels: {
112
+ ...insideOptions,
113
+ align: 'right',
114
+ },
115
+ },
116
+ false,
117
+ );
118
+ }
119
+ } else {
120
+ point.update({ dataLabels: outsideOptions }, false);
121
+ }
96
122
  }
97
123
  });
98
124
  });
@@ -102,7 +128,6 @@ class BarChart {
102
128
 
103
129
  getBarChartLabelsInsideOptions = () => ({
104
130
  inside: true,
105
- align: 'right',
106
131
  verticalAlign: 'middle',
107
132
  style: {
108
133
  color: 'white',
@@ -114,6 +139,7 @@ class BarChart {
114
139
  inside: false,
115
140
  align: undefined,
116
141
  verticalAlign: undefined,
142
+ overflow: 'allow',
117
143
  style: {
118
144
  textOutline: 'none',
119
145
  // The design system does not include a semibold font weight, so we use 700 (bold) as an alternative.
@@ -12,6 +12,7 @@ class ChartConstants {
12
12
  estimateLineColor: '#003c57',
13
13
  uncertaintyRangeColor: 'rgba(32, 96, 149, 0.65)',
14
14
  defaultFontSize: '0.875rem', // 14px
15
+ extraLineColor: '#222222',
15
16
  shadingColor: '#ececec',
16
17
  lineMarkerStyles: [
17
18
  {
@@ -65,6 +65,10 @@ class HighchartsBaseChart {
65
65
  this.commonChartOptions = new CommonChartOptions(this.xAxisTickIntervalDesktop, this.yAxisTickIntervalDesktop);
66
66
  this.estimateLineLabel = this.node.dataset.highchartsEstimateLineLabel;
67
67
  this.uncertainyRangeLabel = this.node.dataset.highchartsUncertaintyRangeLabel;
68
+ this.customReferenceLineValue = this.node.dataset.highchartsCustomReferenceLineValue
69
+ ? parseFloat(this.node.dataset.highchartsCustomReferenceLineValue)
70
+ : undefined;
71
+
68
72
  this.specificChartOptions = new SpecificChartOptions(this.theme, this.chartType, this.config);
69
73
  this.lineChart = new LineChart();
70
74
  this.barChart = new BarChart();
@@ -93,6 +97,15 @@ class HighchartsBaseChart {
93
97
  return this.chartType === 'line' ? 0 : this.config.series.filter((series) => series.type === 'line').length;
94
98
  };
95
99
 
100
+ // Used to ensure that extra line series always overlay the column series
101
+ updateExtraLineZIndex = () => {
102
+ this.config.series.forEach((series) => {
103
+ if (series.type === 'line') {
104
+ series.zIndex = this.config.series.length + 1;
105
+ }
106
+ });
107
+ };
108
+
96
109
  // Check for the number of extra line series in the config
97
110
  checkForExtraScatter = () => {
98
111
  return this.chartType === 'scatter' ? 0 : this.config.series.filter((series) => series.type === 'scatter').length;
@@ -148,7 +161,9 @@ class HighchartsBaseChart {
148
161
  }
149
162
 
150
163
  if (this.extraLines > 0) {
164
+ this.updateExtraLineZIndex();
151
165
  this.config = mergeConfigs(this.config, this.lineChart.getLineChartOptions());
166
+ this.config = mergeConfigs(this.config, this.lineChart.getExtraLineChartOptions(this.config.series.length + 1));
152
167
  if (this.chartType === 'column') {
153
168
  this.config = mergeConfigs(this.config, columnChartOptions);
154
169
  }
@@ -192,8 +207,9 @@ class HighchartsBaseChart {
192
207
  this.rangeAnnotations,
193
208
  this.rangeAnnotationsOptions,
194
209
  this.referenceLineAnnotationsOptions,
195
- this.commonChartOptions,
210
+ this.specificChartOptions,
196
211
  this.chartType,
212
+ this.customReferenceLineValue,
197
213
  );
198
214
 
199
215
  let rules = [
@@ -113,8 +113,6 @@ class CommonChartOptions {
113
113
  },
114
114
  plotOptions: {
115
115
  series: {
116
- // disables the tooltip on hover
117
- enableMouseTracking: false,
118
116
  animation: false,
119
117
 
120
118
  // disables the legend item hover
@@ -129,32 +127,19 @@ class CommonChartOptions {
129
127
  },
130
128
  },
131
129
  },
130
+ tooltip: {
131
+ animation: false,
132
+ },
132
133
  };
133
134
  }
134
135
 
135
136
  getOptions = () => this.options;
136
137
 
137
- // TODO: A future ticket will add support for other plot lines which are not
138
- // reference line annotations, and will be styled like the zero line
139
- // See ticket https://jira.ons.gov.uk/browse/CCB-63
140
- getPlotLines = () => {
141
- // Add zero line
142
- return {
143
- yAxis: {
144
- plotLines: [
145
- {
146
- color: this.constants.zeroLineColor,
147
- width: 1.5,
148
- value: 0,
149
- zIndex: 2,
150
- },
151
- ],
152
- },
153
- };
154
- };
155
-
156
138
  getMobileOptions = (xAxisTickInterval, yAxisTickInterval) => {
157
139
  return {
140
+ tooltip: {
141
+ enabled: false,
142
+ },
158
143
  xAxis: {
159
144
  tickInterval: xAxisTickInterval,
160
145
  },
@@ -185,20 +170,64 @@ class CommonChartOptions {
185
170
 
186
171
  updateLegendSymbols = (chart) => {
187
172
  if (chart.legend.options.enabled) {
188
- chart.legend.allItems.forEach((item) => {
173
+ chart.legend.allItems.forEach((item, index) => {
189
174
  const { legendItem, userOptions } = item;
190
175
  const seriesType = userOptions?.type;
191
- // symbol is defined for bar / column series, and line is defined for line series
192
- // if symbol is defined for a line series, it is the marker symbol
193
- const { label, symbol } = legendItem || {};
176
+ const { label, symbol, line } = legendItem || {};
194
177
 
195
178
  if (seriesType === 'line') {
196
- label?.attr({
197
- x: 30, // Adjust label position to account for longer line
179
+ // This is the case for the column plus line chart - the series type is
180
+ // line, but the chart type is column. In this case we show a simple
181
+ // line symbol in the legend, but we need to move the label to the right
182
+ // to account for the longer line symbol
183
+ if (chart.userOptions.chart.type !== 'line') {
184
+ label?.attr({
185
+ x: 30, // Adjust label position to account for longer line
186
+ });
187
+ }
188
+
189
+ // This is the scenario for a line chart with markers disabled
190
+ // We have custom code in line-chart.js to update the last point to
191
+ // display as a symbol. This code checks if there is no symbol in the legend
192
+ // (which means it is a line chart with markers disabled)
193
+ // and if so, it updates the legend to display as a symbol rather than as a line
194
+ // We only to this for chart types that are explicitly line charts - i.e. not column with line
195
+ if (!symbol && label && label.element && chart.userOptions.chart.type === 'line') {
196
+ // Hide the line in the legend
197
+ if (line) {
198
+ line.hide();
199
+ }
200
+
201
+ // Create a custom symbol for the legend using the line marker symbol options
202
+ const renderer = chart.renderer;
203
+ const bbox = label.element.getBBox();
204
+ const markerStyle = this.constants.lineMarkerStyles[index % this.constants.lineMarkerStyles.length];
205
+
206
+ const legendSymbol = renderer
207
+ .symbol(markerStyle.symbol, bbox.x - 30, bbox.y + 4, 12, markerStyle.radius, markerStyle.radius)
208
+ .attr({
209
+ fill: item.color,
210
+ stroke: item.color,
211
+ 'stroke-width': 1,
212
+ width: 12,
213
+ height: 12,
214
+ });
215
+
216
+ legendSymbol.add(label.parentGroup);
217
+ label?.attr({
218
+ x: 15, // Adjust label position to account for shorter space that the symbol takes up
219
+ });
220
+ }
221
+ } else if (seriesType === 'columnrange') {
222
+ symbol.attr({
223
+ width: 14,
224
+ height: 14,
225
+ y: 8,
198
226
  });
199
227
  } else {
200
228
  if (!symbol) return;
201
- // Set the symbol size for bar / column series
229
+ // Update the symbol width and height
230
+ // For column, bar and other chart types
202
231
  else {
203
232
  symbol.attr({
204
233
  width: 12,
@@ -0,0 +1,56 @@
1
+ {% from "components/chart/_macro.njk" import onsChart %}
2
+
3
+ {{
4
+ onsChart({
5
+ "chartType": "column",
6
+ "description": "Public sector net debt excluding public sector banks, percentage of gross domestic product (GDP), UK, financial year ending (FYE) 1901 to October 2024",
7
+ "theme": "primary",
8
+ "title": "Figure 6: Net debt as a percentage of GDP remains at levels last seen in the early 1960s",
9
+ "subtitle": "Public sector net debt excluding public sector banks, percentage of gross domestic product (GDP), UK, financial year ending (FYE) 1901 to October 2024",
10
+ "id": "uuid",
11
+ "caption": "Source: Public sector finances from the Office for Budget Responsibility (OBR) and the Office for National Statistics",
12
+ "download": {
13
+ 'title': 'Download Figure 1 data',
14
+ 'itemsList': [
15
+ {
16
+ "text": "Excel spreadsheet (XLSX format, 18KB)",
17
+ "url": "#"
18
+ },
19
+ {
20
+ "text": "Simple text file (CSV format, 25KB)",
21
+ "url": "#"
22
+ },
23
+ {
24
+ "text": "Image (PNG format, 25KB)",
25
+ "url": "#"
26
+ }
27
+ ]
28
+ },
29
+ "legend": false,
30
+ "series": [
31
+ {
32
+ "data": [
33
+ 37.8, 41.0, 43.0, 42.9, 41.8, 39.8, 37.9, 38.2, 37.6, 36.7,
34
+ 33.9, 31.8
35
+ ],
36
+ "dataLabels": false,
37
+ "name": "Public sector net debt as a % of GDP (PSND)"
38
+ }
39
+ ],
40
+ "xAxis": {
41
+ "categories": [
42
+ "Mar 1901", "Mar 1902", "Mar 1903", "Mar 1904", "Mar 1905", "Mar 1906", "Mar 1907", "Mar 1908", "Mar 1909", "Mar 1910",
43
+ "Mar 1911", "Mar 1912"
44
+ ],
45
+ "title": "Years",
46
+ "type": "linear",
47
+ "tickIntervalMobile": 5
48
+ },
49
+ "yAxis": {
50
+ "title": "Percentage of GDP",
51
+ "customReferenceLineValue": 25
52
+ },
53
+ "percentageHeightDesktop": 35,
54
+ "percentageHeightMobile": 90
55
+ })
56
+ }}