Skip to content

Commit

Permalink
feat(CodeSnippet): add scroll overflow indicators (#7229)
Browse files Browse the repository at this point in the history
* feat(CodeSnippet): add scroll overflow indicators

* fix(code-snippet): increase expand and copy button z-index

* fix(code-snippet): update overflow indicator token colors

* fix(code-snippet): increase overflow indicator width

* refactor(code-snippet): replace pseudoelement

* fix(code-snippet): consolidate spacing rules on Safari

* fix(code-snippet): separate display rules for single and multiline

* fix(code-snippet): place single line fade effect within focus border

* docs(CodeSnippet): increase length of single line example code

* fix(CodeSnippet): shift overflow indicators with focus outline presence

* fix(code-snippet): reorder overflow indicators based on markup

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
emyarod and kodiakhq[bot] committed Nov 13, 2020
1 parent d27b3d3 commit 5028d80
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 21 deletions.
92 changes: 80 additions & 12 deletions packages/components/src/components/code-snippet/_code-snippet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@
.#{$prefix}--snippet--single {
@include bx--snippet;

display: flex;
align-items: center;
min-width: rem(320px);
max-width: rem(760px);
height: $carbon--spacing-08;
Expand All @@ -133,7 +135,6 @@
height: 100%;
padding-left: $carbon--spacing-05;
overflow-x: auto;
border-right: solid $carbon--spacing-05 transparent;

&:focus {
@include focus-outline('outline');
Expand All @@ -147,21 +148,11 @@
white-space: nowrap;
}

.#{$prefix}--snippet--single::after {
position: absolute;
top: 0;
right: rem(56px);
width: rem(16px);
height: 100%;
// Safari interprets `transparent` differently, so make color token value transparent instead:
background-image: linear-gradient(to right, rgba($field-01, 0), $field-01);
content: '';
}

// Multi Line Snippet
.#{$prefix}--snippet--multi {
@include bx--snippet;

display: flex;
min-width: rem(320px);
max-width: 100%;
padding: $carbon--spacing-05;
Expand All @@ -170,6 +161,7 @@
//closed snippet container
.#{$prefix}--snippet--multi .#{$prefix}--snippet-container {
position: relative;
order: 1;
min-height: rem(56px);
max-height: rem(238px);
overflow: hidden;
Expand Down Expand Up @@ -298,6 +290,7 @@
position: absolute;
top: 0;
right: 0;
z-index: 10;

// Override inherited rule in code snippet
@include carbon--font-family('sans');
Expand All @@ -311,6 +304,7 @@
position: absolute;
right: 0;
bottom: $spacing-03;
z-index: 10;
display: inline-flex;
align-items: center;
padding: $spacing-03 $spacing-05;
Expand Down Expand Up @@ -447,6 +441,80 @@
left: 50%;
}

// overflow indicator
.#{$prefix}--snippet__overflow-indicator--left,
.#{$prefix}--snippet__overflow-indicator--right {
z-index: 1;
flex: 1 0 auto;
width: $carbon--spacing-05;
}

.#{$prefix}--snippet__overflow-indicator--left {
order: 0;
margin-right: -$carbon--spacing-05;
background-image: linear-gradient(to left, transparent, $field-01);
}

.#{$prefix}--snippet__overflow-indicator--right {
order: 2;
margin-left: -$carbon--spacing-05;
background-image: linear-gradient(to right, transparent, $field-01);
}

.#{$prefix}--snippet--single .#{$prefix}--snippet__overflow-indicator--right,
.#{$prefix}--snippet--single .#{$prefix}--snippet__overflow-indicator--left {
position: absolute;
width: $carbon--spacing-07;
height: calc(100% - #{$carbon--spacing-02});
}

.#{$prefix}--snippet--single .#{$prefix}--snippet__overflow-indicator--right {
right: $carbon--spacing-08;
}

.#{$prefix}--snippet--single
.#{$prefix}--snippet-container:focus
~ .#{$prefix}--snippet__overflow-indicator--right {
right: calc(#{$carbon--spacing-08} + #{rem(2px)});
}

.#{$prefix}--snippet--single
.#{$prefix}--snippet-container:focus
+ .#{$prefix}--snippet__overflow-indicator--left {
left: rem(2px);
}

.#{$prefix}--snippet--light .#{$prefix}--snippet__overflow-indicator--left {
background-image: linear-gradient(to left, transparent, $field-02);
}

.#{$prefix}--snippet--light .#{$prefix}--snippet__overflow-indicator--right {
background-image: linear-gradient(to right, transparent, $field-02);
}

// Safari-only media query
// since fades won't appear correctly with CSS custom properties
// see: tabs, code snippet, and modal overflow indicators
@media not all and (min-resolution: 0.001dpcm) {
@supports (-webkit-appearance: none) and (stroke-color: transparent) {
.#{$prefix}--snippet__overflow-indicator--left {
background-image: linear-gradient(
to left,
rgba($field-01, 0),
$field-01
);
}

.#{$prefix}--snippet__overflow-indicator--right {
background-image: linear-gradient(
to right,
rgba($field-01, 0),
$field-01
);
}
}
}

#{$prefix}--snippet--multi.#{$prefix}--skeleton {
height: rem(98px);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export const multiline = () => (

export const singleline = () => (
<CodeSnippet type="single" feedback="Copied to clipboard">
{'node -v'}
yarn add carbon-components@latest carbon-components-react@latest
@carbon/icons-react@latest carbon-icons@latest
</CodeSnippet>
);

Expand Down
90 changes: 82 additions & 8 deletions packages/react/src/components/CodeSnippet/CodeSnippet.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

import PropTypes from 'prop-types';
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import classNames from 'classnames';
import useResizeObserver from 'use-resize-observer/polyfilled';
import debounce from 'lodash.debounce';
import { ChevronDown16 } from '@carbon/icons-react';
import { settings } from 'carbon-components';
import Copy from '../Copy';
Expand Down Expand Up @@ -38,19 +39,76 @@ function CodeSnippet({
const [shouldShowMoreLessBtn, setShouldShowMoreLessBtn] = useState(false);
const { current: uid } = useRef(getUniqueId());
const codeContentRef = useRef();
const codeContainerRef = useRef();
const [hasLeftOverflow, setHasLeftOverflow] = useState(false);
const [hasRightOverflow, setHasRightOverflow] = useState(false);
const getCodeRef = useCallback(() => {
if (type === 'single') {
return codeContainerRef;
}
if (type === 'multi') {
return codeContentRef;
}
}, [type]);

const getCodeRefDimensions = useCallback(() => {
const {
clientWidth: codeClientWidth,
scrollLeft: codeScrollLeft,
scrollWidth: codeScrollWidth,
} = getCodeRef().current;

return {
horizontalOverflow: codeScrollWidth > codeClientWidth,
codeClientWidth,
codeScrollWidth,
codeScrollLeft,
};
}, [getCodeRef]);

const handleScroll = useCallback(() => {
if (
type === 'inline' ||
(type === 'single' && !codeContainerRef?.current) ||
(type === 'multi' && !codeContentRef?.current)
) {
return;
}

const {
horizontalOverflow,
codeClientWidth,
codeScrollWidth,
codeScrollLeft,
} = getCodeRefDimensions();

setHasLeftOverflow(horizontalOverflow && !!codeScrollLeft);
setHasRightOverflow(
horizontalOverflow && codeScrollLeft + codeClientWidth !== codeScrollWidth
);
}, [type, getCodeRefDimensions]);

useResizeObserver({
ref: codeContentRef,
ref: getCodeRef(),
onResize: () => {
if (codeContentRef.current) {
if (codeContentRef?.current && type === 'multi') {
const { height } = codeContentRef.current.getBoundingClientRect();
setShouldShowMoreLessBtn(type === 'multi' && height > 255);
setShouldShowMoreLessBtn(height > 255);
}
if (
(codeContentRef?.current && type === 'multi') ||
(codeContainerRef?.current && type === 'single')
) {
debounce(handleScroll, 200);
}
},
});

const codeSnippetClasses = classNames(className, {
[`${prefix}--snippet`]: true,
useEffect(() => {
handleScroll();
}, [handleScroll]);

const codeSnippetClasses = classNames(className, `${prefix}--snippet`, {
[`${prefix}--snippet--${type}`]: type,
[`${prefix}--snippet--expand`]: expandedCode,
[`${prefix}--snippet--light`]: light,
Expand Down Expand Up @@ -85,14 +143,30 @@ function CodeSnippet({
return (
<div {...rest} className={codeSnippetClasses}>
<div
ref={codeContainerRef}
role={type === 'single' ? 'textbox' : null}
tabIndex={type === 'single' ? 0 : null}
className={`${prefix}--snippet-container`}
aria-label={ariaLabel || copyLabel || 'code-snippet'}>
aria-label={ariaLabel || copyLabel || 'code-snippet'}
onScroll={(type === 'single' && handleScroll) || null}>
<code>
<pre ref={codeContentRef}>{children}</pre>
<pre
ref={codeContentRef}
onScroll={(type === 'multi' && handleScroll) || null}>
{children}
</pre>
</code>
</div>
{/**
* left overflow indicator must come after the snippet due to z-index and
* snippet focus border overlap
*/}
{hasLeftOverflow && (
<div className={`${prefix}--snippet__overflow-indicator--left`} />
)}
{hasRightOverflow && (
<div className={`${prefix}--snippet__overflow-indicator--right`} />
)}
{!hideCopyButton && (
<CopyButton
onClick={onClick}
Expand Down

0 comments on commit 5028d80

Please sign in to comment.