Skip to content

Commit

Permalink
fix(CodeSnippet): use tooltip styles for feedback and add animation (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
emyarod authored and joshblack committed Jan 14, 2020
1 parent 7e91ef8 commit 62217bd
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 136 deletions.
43 changes: 43 additions & 0 deletions packages/components/src/components/code-snippet/_code-snippet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
@import '../../globals/scss/vendor/@carbon/elements/scss/import-once/import-once';
@import '../../globals/scss/css--reset';
@import '../../globals/scss/theme-tokens';
@import '../../globals/scss/tooltip';
@import '../../globals/scss/keyframes';
@import 'mixins';

/// Code snippet styles
Expand Down Expand Up @@ -50,6 +52,37 @@
outline: none;
border: 2px solid $focus;
}

&::before {
@include tooltip--caret;
display: none;
}

.#{$prefix}--copy-btn__feedback {
@include tooltip--content('icon');
clip: auto;
margin: auto;
overflow: visible;
display: none;
}
@include tooltip--placement('icon', 'bottom', 'center');

&.#{$prefix}--copy-btn--animating::before,
&.#{$prefix}--copy-btn--animating .#{$prefix}--copy-btn__feedback {
display: block;
}

&.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-out::before,
&.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-out
.#{$prefix}--copy-btn__feedback {
animation: $duration--fast-02 motion(standard, productive) hide-feedback;
}

&.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-in::before,
&.#{$prefix}--copy-btn--animating.#{$prefix}--copy-btn--fade-in
.#{$prefix}--copy-btn__feedback {
animation: $duration--fast-02 motion(standard, productive) show-feedback;
}
}

.#{$prefix}--snippet--inline code {
Expand Down Expand Up @@ -213,6 +246,16 @@
border: none;
}

// TODO: remove copy button styles above
.#{$prefix}--snippet .#{$prefix}--copy-btn {
position: absolute;
top: 0;
right: 0;
@include carbon--font-family(
'sans'
); // Override inherited rule in code snippet
}

// Show more / less button
button.#{$prefix}--btn.#{$prefix}--snippet-btn--expand {
display: inline-flex;
Expand Down
38 changes: 21 additions & 17 deletions packages/components/src/components/code-snippet/code-snippet.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@

{{#is variant "inline"}}
<p>Here is an example of a text that a user would be reading. In this paragraph would be
<button data-copy-btn="" type="button" class="{{@root.prefix}}--snippet {{@root.prefix}}--snippet--inline {{@root.prefix}}--btn--copy{{#if light}} {{@root.prefix}}--snippet--light{{/if}}"
<button data-copy-btn type="button"
class="{{@root.prefix}}--snippet {{@root.prefix}}--snippet--inline {{#if light}} {{@root.prefix}}--snippet--light{{/if}}"
aria-label="Copy code" tabindex="0">
<code>inline code</code>
<div class="{{@root.prefix}}--btn--copy__feedback" role="alert" data-feedback="Copied!"></div>
<span class="{{@root.prefix}}--assistive-text {{@root.prefix}}--copy-btn__feedback">Copied!</span>
</button>
that the user could look at and copy in to their code editor.</p>
{{else}}

<div class="{{@root.prefix}}--snippet {{@root.prefix}}--snippet--{{variant}}" {{#is variant "multi"}}
data-code-snippet{{/is}}> <div class="{{@root.prefix}}--snippet-container" aria-label="Code Snippet Text">
<pre>
data-code-snippet{{/is}}>
<div class="{{@root.prefix}}--snippet-container" aria-label="Code Snippet Text">
<pre>
<code>@mixin grid-container {
width: 100%;
padding-right: padding(mobile);
Expand All @@ -40,22 +42,24 @@
floating: 10000
);</code>
</pre>
</div>
<button data-copy-btn class="{{@root.prefix}}--snippet-button" type="button" aria-label="Copy" tabindex="0">
{{ carbon-icon 'Copy16' class=(add @root.prefix '--snippet__icon')}}
<div class="{{@root.prefix}}--btn--copy__feedback" role="alert" data-feedback="Copied!"></div>
</button>
{{#is variant "multi"}}
<button class="{{@root.prefix}}--btn {{@root.prefix}}--btn--ghost {{@root.prefix}}--btn--sm {{@root.prefix}}--snippet-btn--expand"
type="button">
<span class="{{@root.prefix}}--snippet-btn--text" data-show-more-text="Show more" data-show-less-text="Show less">Show
more</span>
{{ carbon-icon 'ChevronDown16' class=(add
</div>
<button data-copy-btn class="{{@root.prefix}}--copy-btn" type="button" tabindex="0">
<span class="{{@root.prefix}}--assistive-text {{@root.prefix}}--copy-btn__feedback">Copied!</span>
{{ carbon-icon 'Copy16' class=(add @root.prefix '--snippet__icon' hidden="true")}}
</button>
{{#is variant "multi"}}
<button
class="{{@root.prefix}}--btn {{@root.prefix}}--btn--ghost {{@root.prefix}}--btn--sm {{@root.prefix}}--snippet-btn--expand"
type="button">
<span class="{{@root.prefix}}--snippet-btn--text" data-show-more-text="Show more"
data-show-less-text="Show less">Show
more</span>
{{ carbon-icon 'ChevronDown16' class=(add
(add @root.prefix '--icon-chevron--down')
(add ' ' (add @root.prefix '--snippet__icon'))
)
aria-label="Show more icon"}}
</button>
{{/is}}
</button>
{{/is}}
</div>
{{/is}}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
@import '../../globals/scss/vendor/@carbon/elements/scss/import-once/import-once';
@import '../../globals/scss/css--reset';
@import '../button/button';
@import 'keyframes';
@import '../../globals/scss/tooltip';
@import '../../globals/scss/keyframes';

@include exports('copy-button') {
.#{$prefix}--btn--copy {
Expand Down Expand Up @@ -85,6 +86,11 @@
height: $carbon--spacing-08;
width: $carbon--spacing-08;
background-color: $ui-01;
cursor: pointer;

&:hover {
background-color: $hover-ui;
}

&::before {
@include tooltip--caret;
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/components/CodeSnippet/CodeSnippet.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ function CodeSnippet({
</code>
</div>
<CopyButton
className={`${prefix}--snippet-button`}
onClick={onClick}
feedback={feedback}
iconDescription={copyButtonDescription}
Expand Down
30 changes: 12 additions & 18 deletions packages/react/src/components/Copy/Copy-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,38 +52,32 @@ describe('Copy', () => {
it('Should be able to specify the feedback message', () => {
const feedbackWrapper = mount(<Copy feedback="Copied!" />);
expect(
feedbackWrapper.find(`.${prefix}--btn--copy__feedback`).props()[
'data-feedback'
]
feedbackWrapper.find(`.${prefix}--copy-btn__feedback`).text()
).toBe('Copied!');
});
});

describe('Renders feedback as expected', () => {
it('Should make the feedback visible', () => {
const feedbackWrapper = mount(<Copy feedback="Copied!" />);
const feedback = () =>
feedbackWrapper.find(`.${prefix}--btn--copy__feedback`);
expect(
feedback().hasClass(`${prefix}--btn--copy__feedback--displayed`)
).toBe(false);
feedbackWrapper.setState({ showFeedback: true });
expect(
feedback().hasClass(`${prefix}--btn--copy__feedback--displayed`)
).toBe(true);
const feedback = feedbackWrapper.find(`.${prefix}--copy-btn__feedback`);
expect(feedback).toBeFalsy;
feedbackWrapper.simulate('click');
expect(feedback).toBeTruthy;
});

it('Should show feedback for a limited amount of time', () => {
const feedbackWrapper = mount(
<Copy feedback="Copied!" feedbackTimeout={5000} />
);
expect(feedbackWrapper.state().showFeedback).toBe(false);
feedbackWrapper.simulate('click');
expect(feedbackWrapper.state().showFeedback).toBe(true);
expect(setTimeout.mock.calls.length).toBe(2);
expect(setTimeout.mock.calls[1][1]).toBe(5000);
jest.runAllTimers();
expect(feedbackWrapper.state().showFeedback).toBe(false);
const copyButton = feedbackWrapper.find('button');
expect(copyButton.hasClass(`${prefix}--copy-btn--animating`)).toBe(true);
setTimeout(() => {
expect(copyButton.hasClass(`${prefix}--copy-btn--animating`)).toBe(
false
);
}, 5220); // 5000 + 2 * 110 (transition duration)
});
});

Expand Down
160 changes: 85 additions & 75 deletions packages/react/src/components/Copy/Copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,91 +6,101 @@
*/

import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import debounce from 'lodash.debounce';
import classnames from 'classnames';
import { settings } from 'carbon-components';
import { composeEventHandlers } from '../../tools/events';

const { prefix } = settings;

export default class Copy extends Component {
static propTypes = {
/**
* Pass in content to be rendred in the underlying <button>
*/
children: PropTypes.node,

/**
* Specify an optional className to be applied to the underlying <button>
*/
className: PropTypes.string,

/**
* Specify the string that is displayed when the button is clicked and the
* content is copied
*/
feedback: PropTypes.string,

/**
* Specify the time it takes for the feedback message to timeout
*/
feedbackTimeout: PropTypes.number,

/**
* Specify an optional `onClick` handler that is called when the underlying
* <button> is clicked
*/
onClick: PropTypes.func,
export default function Copy({
children,
className,
feedback,
feedbackTimeout,
onAnimationEnd,
onClick,
...other
}) {
const [animation, setAnimation] = useState('');
const classNames = classnames(className, {
[`${prefix}--copy-btn--animating`]: animation,
[`${prefix}--copy-btn--${animation}`]: animation,
});
const handleFadeOut = useCallback(
debounce(() => {
setAnimation('fade-out');
}, feedbackTimeout),
[feedbackTimeout]
);
const handleClick = useCallback(() => {
setAnimation('fade-in');
handleFadeOut();
}, [handleFadeOut]);
const handleAnimationEnd = event => {
if (event.animationName === 'hide-feedback') {
setAnimation('');
}
};

static defaultProps = {
feedback: 'Copied!',
feedbackTimeout: 2000,
onClick: () => {},
};
useEffect(
() => () => {
handleFadeOut.cancel();
},
[handleFadeOut]
);

state = {
showFeedback: false,
};
return (
<button
type="button"
className={classNames}
onClick={composeEventHandlers([onClick, handleClick])}
onAnimationEnd={composeEventHandlers([
onAnimationEnd,
handleAnimationEnd,
])}
{...other}>
{children}
<span
className={`${prefix}--assistive-text ${prefix}--copy-btn__feedback`}>
{feedback}
</span>
</button>
);
}

/* istanbul ignore next */
componentWillUnmount() {
if (typeof this.timeoutId !== 'undefined') {
clearTimeout(this.timeoutId);
delete this.timeoutId;
}
}
Copy.propTypes = {
/**
* Pass in content to be rendred in the underlying <button>
*/
children: PropTypes.node,

handleClick = evt => {
this.setState({ showFeedback: true });
this.timeoutId = setTimeout(() => {
this.setState({ showFeedback: false });
}, this.props.feedbackTimeout);
/**
* Specify an optional className to be applied to the underlying <button>
*/
className: PropTypes.string,

this.props.onClick(evt);
}; // eslint-disable-line no-unused-vars
/**
* Specify the string that is displayed when the button is clicked and the
* content is copied
*/
feedback: PropTypes.string,

render() {
const {
className,
feedback,
children,
feedbackTimeout, // eslint-disable-line no-unused-vars
onClick, // eslint-disable-line no-unused-vars
...other
} = this.props;
const feedbackClassNames = classnames(`${prefix}--btn--copy__feedback`, {
[`${prefix}--btn--copy__feedback--displayed`]: this.state.showFeedback,
});
/**
* Specify the time it takes for the feedback message to timeout
*/
feedbackTimeout: PropTypes.number,

return (
<button
type="button"
className={className}
onClick={this.handleClick}
{...other}>
{children}
<div className={feedbackClassNames} data-feedback={feedback} />
</button>
);
}
}
/**
* Specify an optional `onClick` handler that is called when the underlying
* <button> is clicked
*/
onClick: PropTypes.func,
};

Copy.defaultProps = {
feedback: 'Copied!',
feedbackTimeout: 2000,
onClick: () => {},
};
Loading

0 comments on commit 62217bd

Please sign in to comment.