diff --git a/.changeset/selfish-apes-breathe.md b/.changeset/selfish-apes-breathe.md new file mode 100644 index 00000000000..46717e5ad0c --- /dev/null +++ b/.changeset/selfish-apes-breathe.md @@ -0,0 +1,5 @@ +--- +"@hashicorp/design-system-components": patch +--- + +`CodeBlock` - Fixed issues with line numbers when line wrapping is present and when the number of lines changes dynamically; line highlighting when the Code Block is hidden from view initially such as when used inside a Tabs component; and line highlighting when hasLineNumbers is false. diff --git a/packages/components/src/components/hds/code-block/index.hbs b/packages/components/src/components/hds/code-block/index.hbs index ea0531a04b4..931ef1dd859 100644 --- a/packages/components/src/components/hds/code-block/index.hbs +++ b/packages/components/src/components/hds/code-block/index.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: MPL-2.0 }} -
+
{{~yield (hash Title=(component "hds/code-block/title"))~}} {{~yield (hash Description=(component "hds/code-block/description"))~}} @@ -17,7 +17,7 @@ data-start={{@lineNumberStart}} id={{this._preCodeId}} tabindex="0" - > + > {{~this._prismCode~}} diff --git a/packages/components/src/components/hds/code-block/index.ts b/packages/components/src/components/hds/code-block/index.ts index 07bd750aa4c..a2cbbd70f46 100644 --- a/packages/components/src/components/hds/code-block/index.ts +++ b/packages/components/src/components/hds/code-block/index.ts @@ -10,6 +10,7 @@ import { assert } from '@ember/debug'; import { next, schedule } from '@ember/runloop'; import { htmlSafe } from '@ember/template'; import { guidFor } from '@ember/object/internals'; +import { modifier } from 'ember-modifier'; import Prism from 'prismjs'; @@ -75,6 +76,34 @@ export default class HdsCodeBlock extends Component { // Generates a unique ID for the code content private _preCodeId = 'pre-code-' + guidFor(this); + private _preCodeElement!: HTMLPreElement; + private _observer!: ResizeObserver; + + // If a code block is hidden from view, and made visible after load, the Prism code needs to be re-run + private _setUpCodeObserver = modifier((element: HTMLElement) => { + this._preCodeElement = element.querySelector( + '.hds-code-block__code' + ) as HTMLPreElement; + this._observer = new ResizeObserver((entries) => { + entries.forEach((entry) => { + if (entry.contentBoxSize) { + this._updateCodeHeights(); + this._updatePrismPlugins(); + } + }); + }); + this._observer.observe(element); + + return () => { + this._observer.disconnect(); + }; + }); + + private _setUpCodeBlockCode = modifier((element: HTMLElement) => { + this._isExpanded = false; // reset expanded state on updates + this.setPrismCode(element); + return () => {}; + }); // code text content for the CodeBlock get code(): string { @@ -145,6 +174,14 @@ export default class HdsCodeBlock extends Component { this._prismCode = htmlSafe(Prism.util.encode(code).toString()); } + // Existing line numbers must be removed in order to be updated correctly + const lineNumbers = element.querySelector( + '.line-numbers-rows' + ) as HTMLElement; + if (lineNumbers) { + element.removeChild(lineNumbers); + } + // Force prism-line-numbers plugin initialization, required for Prism.highlight usage // See https://github.com/PrismJS/prism/issues/1234 Prism.hooks.run('complete', { @@ -152,28 +189,38 @@ export default class HdsCodeBlock extends Component { element, }); - // Get the actual height & the content height of the preCodeElement // eslint-disable-next-line ember/no-runloop schedule('afterRender', (): void => { - const preCodeElement = document.getElementById(this._preCodeId); - this._codeContentHeight = preCodeElement?.scrollHeight ?? 0; - this._codeContainerHeight = preCodeElement?.clientHeight ?? 0; + this._updateCodeHeights(); + // we need to delay re-evaluating the context for prism plugins for as much as possible, and `afterRender` is the 'latest' we can use in the component lifecycle + this._updatePrismPlugins(); }); - - // Force prism-line-highlight plugin initialization - // Context: https://github.com/hashicorp/design-system/pull/1749#discussion_r1374288785 - if (this.args.highlightLines) { - // 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 - // eslint-disable-next-line ember/no-runloop - schedule('afterRender', (): void => { - // 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 - if (window) window.dispatchEvent(new Event('resize')); - }); - } }); } } + private _updateCodeHeights(): void { + if (!this._isExpanded) { + // Get the actual height & the content height of the preCodeElement + this._codeContentHeight = this._preCodeElement?.scrollHeight ?? 0; + this._codeContainerHeight = this._preCodeElement?.clientHeight ?? 0; + } + } + + private _updatePrismPlugins(): void { + if (this.hasLineNumbers && Prism?.plugins?.['lineNumbers']) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + Prism.plugins['lineNumbers'].resize(this._preCodeElement); + } + + // Force prism-line-highlight plugin initialization + // Context: https://github.com/hashicorp/design-system/pull/1749#discussion_r1374288785 + if (this.args.highlightLines) { + // 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 + if (window) window.dispatchEvent(new Event('resize')); + } + } + @action toggleExpanded(): void { this._isExpanded = !this._isExpanded; diff --git a/packages/components/src/styles/components/code-block/index.scss b/packages/components/src/styles/components/code-block/index.scss index c995c208ac2..c5f24efc996 100644 --- a/packages/components/src/styles/components/code-block/index.scss +++ b/packages/components/src/styles/components/code-block/index.scss @@ -147,6 +147,7 @@ $hds-code-block-code-footer-height: 48px; } code { + position: relative; display: inline-block; padding-right: $hds-code-block-code-padding; } @@ -220,6 +221,13 @@ $hds-code-block-code-footer-height: 48px; position: relative; // reserve space for line numbers padding-left: calc(#{$hds-code-block-line-numbers-width} + #{$hds-code-block-code-padding}); + + // When line numbers are enabled, line highlighing is calculated based on the pre element instead of the code element + // To ensure the offset is correct, we need to set the position of the code element to static + // Source: https://github.com/PrismJS/prism/blob/v2/src/plugins/line-highlight/prism-line-highlight.ts#L92 + code { + position: static; + } } .hds-code-block__overlay-footer { @@ -250,15 +258,17 @@ $hds-code-block-code-footer-height: 48px; } } } + + .line-highlight { + left: 0; + } } // Highlighted Lines .line-highlight { position: absolute; right: 0; - left: 0; - // Note: position seems off by a few px although not sure why - margin-top: -3px; + left: -$hds-code-block-code-padding; background-color: var(--hds-code-block-color-line-highlight); border: solid var(--hds-code-block-color-line-highlight-border); border-width: 1px 0 1px 4px; diff --git a/showcase/app/controllers/components/code-block.js b/showcase/app/controllers/components/code-block.js index 4748833804f..45ea5c04b91 100644 --- a/showcase/app/controllers/components/code-block.js +++ b/showcase/app/controllers/components/code-block.js @@ -31,6 +31,25 @@ export default class CodeBlockController extends Controller { @tracked isModalActive = false; @tracked declaration = 'let'; @tracked input = ''; + @tracked value_demo1 = this.value_start_demo1; + + value_start_demo1 = `package main +import 'fmt' +func main() { + res = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam' + fm.Println(res) +}`; + + value_new_demo1 = `package main +import 'fmt' +func main() { + res = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam' + fm.Println(res) +} +func main2() { + res = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam' + fm.Println(res) +}`; constructor() { super(...arguments); @@ -76,4 +95,13 @@ export default class CodeBlockController extends Controller { deactivateModal() { this.isModalActive = false; } + + @action + onUpdateClickDemo1() { + if (this.value_demo1 === this.value_start_demo1) { + this.value_demo1 = this.value_new_demo1; + } else { + this.value_demo1 = this.value_start_demo1; + } + } } diff --git a/showcase/app/templates/components/code-block.hbs b/showcase/app/templates/components/code-block.hbs index 1108807b4e8..fae8c9f4c9f 100644 --- a/showcase/app/templates/components/code-block.hbs +++ b/showcase/app/templates/components/code-block.hbs @@ -659,6 +659,7 @@ console.log(`I am ${codeLang} code`);" @language="go" @highlightLines="2, 4" @hasLineWrapping={{true}} + @maxHeight="130px" @value="package main import 'fmt' func main() { @@ -720,5 +721,15 @@ func main() { {{/if}} + + + + \ No newline at end of file