Skip to content

Commit 130b668

Browse files
CodeBlock - height toggle feature (HDS-4745) (#2826)
1 parent 0938f2f commit 130b668

File tree

6 files changed

+298
-60
lines changed

6 files changed

+298
-60
lines changed

.changeset/fresh-kangaroos-sing.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hashicorp/design-system-components": minor
3+
---
4+
5+
`CodeBlock` - Added height toggle control which is present when a `maxHeight` is set and code content height exceeds the `maxHeight` value

packages/components/src/components/hds/code-block/index.hbs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
{{! content within pre tag is whitespace-sensitive; do not add new lines! }}
1313
<pre
1414
class="hds-code-block__code"
15-
{{style maxHeight=@maxHeight}}
15+
{{style maxHeight=this.maxHeight}}
1616
data-line={{@highlightLines}}
1717
data-start={{@lineNumberStart}}
1818
id={{this._preCodeId}}
@@ -30,4 +30,16 @@
3030
/>
3131
{{/if}}
3232
</div>
33+
{{#if this.showFooter}}
34+
<div class="hds-code-block__overlay-footer">
35+
<Hds::Button
36+
class="hds-code-block__height-toggle-button"
37+
@text={{if this._isExpanded "Show less code" "Show more code"}}
38+
@color="secondary"
39+
@icon={{if this._isExpanded "unfold-close" "unfold-open"}}
40+
@size="small"
41+
{{on "click" this.toggleExpanded}}
42+
/>
43+
</div>
44+
{{/if}}
3345
</div>

packages/components/src/components/hds/code-block/index.ts

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,14 @@ export interface HdsCodeBlockSignature {
6969

7070
export default class HdsCodeBlock extends Component<HdsCodeBlockSignature> {
7171
@tracked private _prismCode: SafeString = htmlSafe('');
72+
@tracked private _isExpanded: boolean = false;
73+
@tracked private _codeContentHeight: number = 0;
74+
@tracked private _codeContainerHeight: number = 0;
7275

73-
/**
74-
* Generates a unique ID for the code content
75-
*
76-
* @param _preCodeId
77-
*/
76+
// Generates a unique ID for the code content
7877
private _preCodeId = 'pre-code-' + guidFor(this);
7978

80-
/**
81-
* @param code
82-
* @type {string}
83-
* @description code text content for the CodeBlock
84-
*/
79+
// code text content for the CodeBlock
8580
get code(): string {
8681
const code = this.args.value;
8782

@@ -98,42 +93,34 @@ export default class HdsCodeBlock extends Component<HdsCodeBlockSignature> {
9893
return code;
9994
}
10095

101-
/**
102-
* @param language
103-
* @type {string}
104-
* @default undefined
105-
* @description name of coding language used within CodeBlock for syntax highlighting
106-
*/
96+
get maxHeight(): string | undefined {
97+
return this._isExpanded ? 'none' : this.args.maxHeight;
98+
}
99+
100+
// Shows overlay footer if maxHeight is set and the pre tag content height is greater than the pre tag height
101+
get showFooter(): boolean {
102+
if (this.args.maxHeight) {
103+
return this._codeContentHeight > this._codeContainerHeight;
104+
}
105+
return false;
106+
}
107+
108+
// Name of coding language used within CodeBlock for syntax highlighting
107109
get language(): HdsCodeBlockLanguages | undefined {
108110
return this.args.language ?? undefined;
109111
}
110112

111-
/**
112-
* @param hasLineNumbers
113-
* @type {boolean}
114-
* @default true
115-
* @description Displays line numbers if true
116-
*/
113+
// Displays line numbers if true
117114
get hasLineNumbers(): boolean {
118115
return this.args.hasLineNumbers ?? true;
119116
}
120117

121-
/**
122-
* @param isStandalone
123-
* @type {boolean}
124-
* @default true
125-
* @description Make CodeBlock container corners appear rounded
126-
*/
118+
// Make CodeBlock container corners appear rounded (the standalone variation)
127119
get isStandalone(): boolean {
128120
return this.args.isStandalone ?? true;
129121
}
130122

131-
/**
132-
* @param hasLineWrapping
133-
* @type {boolean}
134-
* @default false
135-
* @description Make text content wrap on multiple lines
136-
*/
123+
// Make text content wrap to multiple lines
137124
get hasLineWrapping(): boolean {
138125
return this.args.hasLineWrapping ?? false;
139126
}
@@ -150,7 +137,7 @@ export default class HdsCodeBlock extends Component<HdsCodeBlockSignature> {
150137

151138
if (code) {
152139
// eslint-disable-next-line ember/no-runloop
153-
next(() => {
140+
next((): void => {
154141
if (language && grammar) {
155142
this._prismCode = htmlSafe(Prism.highlight(code, grammar, language));
156143
} else {
@@ -165,12 +152,20 @@ export default class HdsCodeBlock extends Component<HdsCodeBlockSignature> {
165152
element,
166153
});
167154

155+
// Get the actual height & the content height of the preCodeElement
156+
// eslint-disable-next-line ember/no-runloop
157+
schedule('afterRender', (): void => {
158+
const preCodeElement = document.getElementById(this._preCodeId);
159+
this._codeContentHeight = preCodeElement?.scrollHeight ?? 0;
160+
this._codeContainerHeight = preCodeElement?.clientHeight ?? 0;
161+
});
162+
168163
// Force prism-line-highlight plugin initialization
169164
// Context: https://github.com/hashicorp/design-system/pull/1749#discussion_r1374288785
170165
if (this.args.highlightLines) {
171166
// we need to delay re-evaluating the context for prism-line-highlight for as much as possible, and `afterRender` is the 'latest' we can use in the component lifecycle
172167
// eslint-disable-next-line ember/no-runloop
173-
schedule('afterRender', () => {
168+
schedule('afterRender', (): void => {
174169
// we piggy-back on the plugin's `resize` event listener to trigger a new call of the `highlightLines` function: https://github.com/PrismJS/prism/blob/master/plugins/line-highlight/prism-line-highlight.js#L337
175170
if (window) window.dispatchEvent(new Event('resize'));
176171
});
@@ -179,11 +174,11 @@ export default class HdsCodeBlock extends Component<HdsCodeBlockSignature> {
179174
}
180175
}
181176

182-
/**
183-
* Get the class names to apply to the component.
184-
* @method classNames
185-
* @return {string} The "class" attribute to apply to the component.
186-
*/
177+
@action
178+
toggleExpanded(): void {
179+
this._isExpanded = !this._isExpanded;
180+
}
181+
187182
get classNames(): string {
188183
// Currently there is only one theme so the class name is hard-coded.
189184
// In the future, additional themes such as a "light" theme could be added.
@@ -206,6 +201,14 @@ export default class HdsCodeBlock extends Component<HdsCodeBlockSignature> {
206201
classes.push('line-numbers');
207202
}
208203

204+
if (this.showFooter) {
205+
classes.push('hds-code-block--has-overlay-footer');
206+
}
207+
208+
if (this._isExpanded) {
209+
classes.push('hds-code-block--is-expanded');
210+
}
211+
209212
return classes.join(' ');
210213
}
211214
}

packages/components/src/styles/components/code-block/index.scss

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// DIMENSIONS
1616
$hds-code-block-line-numbers-width: 49px; // 3em ≈ 49px
1717
$hds-code-block-code-padding: 16px;
18+
$hds-code-block-code-footer-height: 48px;
1819

1920
// CODE-BLOCK PARENT/WRAPPER
2021

@@ -35,7 +36,6 @@ $hds-code-block-code-padding: 16px;
3536
word-spacing: normal;
3637
tab-size: 4;
3738
hyphens: none;
38-
scrollbar-width: thin;
3939

4040
@media print {
4141
text-shadow: none;
@@ -47,13 +47,15 @@ $hds-code-block-code-padding: 16px;
4747

4848
// isStandalone
4949
.hds-code-block--is-standalone {
50+
overflow: hidden; // hide corners of scrollbar that poke out
5051
border-radius: var(--token-border-radius-medium);
5152
}
5253

5354
// hasLineWrapping
5455
.hds-code-block--has-line-wrapping {
5556
.hds-code-block__code,
5657
.hds-code-block__code code {
58+
display: block;
5759
white-space: pre-wrap;
5860
overflow-wrap: break-word;
5961
}
@@ -128,10 +130,13 @@ $hds-code-block-code-padding: 16px;
128130
// Code
129131
.hds-code-block__code {
130132
@include hds-focus-ring-basic();
131-
display: block;
133+
position: relative;
134+
display: grid;
132135
margin: 0;
133-
padding: $hds-code-block-code-padding;
136+
padding: $hds-code-block-code-padding 0 $hds-code-block-code-padding $hds-code-block-code-padding;
134137
overflow: auto;
138+
scrollbar-width: thin;
139+
scrollbar-color: var(--token-color-palette-neutral-400) var(--token-color-palette-neutral-500);
135140
font-size: 0.8125rem;
136141
font-family: var(--token-typography-code-200-font-family);
137142
border-radius: inherit;
@@ -140,32 +145,38 @@ $hds-code-block-code-padding: 16px;
140145
color: var(--hds-code-block-color-foreground-selection);
141146
background-color: var(--hds-code-block-color-surface-selection);
142147
}
148+
149+
code {
150+
display: inline-block;
151+
padding-right: $hds-code-block-code-padding;
152+
}
143153
}
144154

145-
// CopyButton
155+
// General dark button styles
156+
.hds-code-block__height-toggle-button,
146157
.hds-code-block__copy-button {
147-
position: absolute;
148-
top: 11px; // 12px -1px accounting for border
149-
right: 12px; // 12px -1px accounting for border
150158
// Overriding default colors
151159
color: var(--hds-code-block-color-foreground-primary);
152160
background-color: var(--hds-code-block-color-surface-faint);
153161
border: 1px solid var(--hds-code-block-color-border-strong);
154162

155163
&:hover,
156164
&.mock-hover {
165+
color: var(--hds-code-block-color-foreground-primary);
157166
background-color: var(--hds-code-block-color-surface-primary);
158167
border-color: var(--hds-code-block-color-border-strong);
159168
}
160169

161170
&:active,
162171
&.mock-active {
172+
color: var(--hds-code-block-color-foreground-primary);
163173
background-color: var(--hds-code-block-color-surface-interactive-active);
164174
}
165175

166176
&:focus,
167177
&.mock-focus,
168178
&:focus-visible {
179+
color: var(--hds-code-block-color-foreground-primary);
169180
background-color: var(--hds-code-block-color-surface-faint);
170181
border-color: var(--hds-code-block-color-focus-action-internal);
171182

@@ -174,6 +185,17 @@ $hds-code-block-code-padding: 16px;
174185
}
175186
}
176187

188+
.hds-icon {
189+
color: var(--hds-code-block-color-foreground-primary);
190+
}
191+
}
192+
193+
// CopyButton
194+
.hds-code-block__copy-button {
195+
position: absolute;
196+
top: 11px; // 12px -1px accounting for border
197+
right: 12px;
198+
177199
&.hds-copy-button--status-success {
178200
.hds-icon {
179201
color: var(--hds-code-block-color-foreground-success);
@@ -185,12 +207,9 @@ $hds-code-block-code-padding: 16px;
185207
color: var(--hds-code-block-color-foreground-critical);
186208
}
187209
}
188-
189-
.hds-icon {
190-
color: var(--hds-code-block-color-foreground-primary);
191-
}
192210
}
193211

212+
// Prism.js plugins
194213
.hds-code-block {
195214
// LineNumbers plugin styles ---------------
196215
// Note: Prism.js is using the specific class name "line-numbers" to determine implementation of line numbers in the UI
@@ -203,6 +222,11 @@ $hds-code-block-code-padding: 16px;
203222
padding-left: calc(#{$hds-code-block-line-numbers-width} + #{$hds-code-block-code-padding});
204223
}
205224

225+
.hds-code-block__overlay-footer {
226+
// match horizontal padding of the code block
227+
margin-left: $hds-code-block-line-numbers-width;
228+
}
229+
206230
.line-numbers-rows {
207231
position: absolute;
208232
top: 0;
@@ -262,3 +286,64 @@ $hds-code-block-code-padding: 16px;
262286
font-style: italic;
263287
}
264288
}
289+
290+
// Footer
291+
.hds-code-block__overlay-footer {
292+
position: absolute;
293+
right: 0;
294+
bottom: 0;
295+
left: 0;
296+
display: flex;
297+
align-items: center;
298+
justify-content: center;
299+
padding: 10px $hds-code-block-code-padding 10px $hds-code-block-code-padding;
300+
pointer-events: none; // fix issue with scrolling when hovering over the footer
301+
302+
// re-enable pointer events for the button (or any content inside the footer)
303+
> * {
304+
pointer-events: auto;
305+
}
306+
}
307+
308+
// Usage of sticky positioning, negative margins, and z-index are required to prevent styling issues
309+
// when horizontal scrolling is present.
310+
// https://hashicorp.slack.com/archives/C025N5V4PFZ/p1746659338984369
311+
.hds-code-block--has-overlay-footer {
312+
.hds-code-block__code::after {
313+
// gradient element
314+
position: sticky; // prevent gradient from scrolling together with content
315+
bottom: -$hds-code-block-code-padding;
316+
left: 0;
317+
display: block;
318+
height: $hds-code-block-code-footer-height;
319+
margin: 0 0 -#{$hds-code-block-code-padding} -1000px; // cover line highlights when line numbers are enabled
320+
background: linear-gradient(360deg, #0d0e12 37.07%, rgba(13, 14, 18, 25%) 100%);
321+
content: "";
322+
pointer-events: none;
323+
}
324+
325+
.line-numbers .line-numbers-rows {
326+
padding-bottom: $hds-code-block-code-footer-height;
327+
}
328+
329+
// place line numbers on top of footer gradient
330+
.line-numbers-rows {
331+
z-index: 1;
332+
isolation: isolate;
333+
}
334+
}
335+
336+
.hds-code-block--is-expanded {
337+
.hds-code-block__code {
338+
// account for the footer at the bottom of the code block
339+
padding-bottom: $hds-code-block-code-footer-height;
340+
341+
&::after {
342+
content: none;
343+
}
344+
}
345+
346+
.hds-code-block__overlay-footer {
347+
background: none;
348+
}
349+
}

0 commit comments

Comments
 (0)