Skip to content

Commit b7d212a

Browse files
committed
merge
2 parents e3148ae + 97f83a0 commit b7d212a

19 files changed

+537
-77
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
2+
import { ComponentFixture, TestBed } from "@angular/core/testing";
3+
import { StaticIconModule } from "../icon/static-icon.module";
4+
import { I18nModule } from "../i18n/i18n.module";
5+
6+
import { CodeSnippet } from "./code-snippet.component";
7+
8+
describe("CodeSnippet", () => {
9+
let component: CodeSnippet;
10+
let fixture: ComponentFixture<CodeSnippet>;
11+
12+
beforeEach(() => {
13+
TestBed.configureTestingModule({
14+
declarations: [CodeSnippet],
15+
imports: [BrowserAnimationsModule, StaticIconModule, I18nModule]
16+
});
17+
18+
fixture = TestBed.createComponent(CodeSnippet);
19+
component = fixture.componentInstance;
20+
});
21+
22+
it("should work", () => {
23+
expect(component instanceof CodeSnippet).toBe(true);
24+
});
25+
});
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import {
2+
Component,
3+
Input,
4+
HostBinding,
5+
ViewChild,
6+
HostListener
7+
} from "@angular/core";
8+
9+
import { I18n } from "../i18n/i18n.module";
10+
11+
export enum SnippetType {
12+
single = "single",
13+
multi = "multi",
14+
inline = "inline"
15+
}
16+
17+
/**
18+
* ```html
19+
* <ibm-code-snippet>Code</ibm-code-snippet>
20+
* ```
21+
* @export
22+
* @class CodeSnippet
23+
*/
24+
@Component({
25+
selector: "ibm-code-snippet",
26+
template: `
27+
<ng-container *ngIf="display === 'inline'; else notInline">
28+
<ng-container *ngTemplateOutlet="codeTemplate"></ng-container>
29+
<ng-container *ngTemplateOutlet="feedbackTemplate"></ng-container>
30+
</ng-container>
31+
32+
<ng-template #notInline>
33+
<div class="bx--snippet-container" [attr.aria-label]="translations.CODE_SNIPPET_TEXT">
34+
<pre><ng-container *ngTemplateOutlet="codeTemplate"></ng-container></pre>
35+
</div>
36+
<button
37+
class="bx--snippet-button"
38+
[attr.aria-label]="translations.COPY_CODE"
39+
(click)="onCopyButtonClicked()"
40+
tabindex="0">
41+
<svg class="bx--snippet__icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
42+
<path d="M1 10H0V2C0 .9.9 0 2 0h8v1H2c-.6 0-1 .5-1 1v8z" />
43+
<path d="M11 4.2V8h3.8L11 4.2zM15 9h-4c-.6 0-1-.4-1-1V4H4.5c-.3 0-.5.2-.5.5v10c0 .3.2.5.5.5h10c.3 0
44+
.5-.2.5-.5V9zm-4-6c.1 0 .3.1.4.1l4.5 4.5c0 .1.1.3.1.4v6.5c0 .8-.7 1.5-1.5 1.5h-10c-.8
45+
0-1.5-.7-1.5-1.5v-10C3 3.7 3.7 3 4.5 3H11z"/>
46+
</svg>
47+
<ng-container *ngTemplateOutlet="feedbackTemplate"></ng-container>
48+
</button>
49+
<button
50+
*ngIf="display === 'multi' && shouldShowExpandButton"
51+
class="bx--btn bx--btn--ghost bx--btn--sm bx--snippet-btn--expand"
52+
(click)="toggleSnippetExpansion()"
53+
type="button">
54+
<span class="bx--snippet-btn--text">{{expanded ? translations.SHOW_LESS : translations.SHOW_MORE}}</span>
55+
<svg
56+
class="bx--icon-chevron--down"
57+
width="12" height="7"
58+
viewBox="0 0 12 7"
59+
[attr.aria-label]="translations.SHOW_MORE_ICON">
60+
<title>{{translations.SHOW_MORE_ICON}}</title>
61+
<path fill-rule="nonzero" d="M6.002 5.55L11.27 0l.726.685L6.003 7 0 .685.726 0z" />
62+
</svg>
63+
</button>
64+
</ng-template>
65+
66+
<ng-template #codeTemplate>
67+
<code #code><ng-content></ng-content></code>
68+
</ng-template>
69+
70+
<ng-template #feedbackTemplate>
71+
<div
72+
class="bx--btn--copy__feedback"
73+
[ngClass]="{
74+
'bx--btn--copy__feedback--displayed': showFeedback
75+
}"
76+
[attr.data-feedback]="feedbackText">
77+
</div>
78+
</ng-template>
79+
`
80+
})
81+
export class CodeSnippet {
82+
/**
83+
* Variable used for creating unique ids for code-snippet components.
84+
* @type {number}
85+
* @static
86+
* @memberof CodeSnippet
87+
*/
88+
static codeSnippetCount = 0;
89+
90+
/**
91+
* It can be `"single"`, `"multi"` or `"inline"`
92+
*
93+
* @type {SnippetType}
94+
* @memberof CodeSnippet
95+
*/
96+
@Input() display: SnippetType = SnippetType.single;
97+
@Input() translations = this.i18n.get().CODE_SNIPPET;
98+
99+
/**
100+
* Text displayed in the tooltip when user clicks button to copy code.
101+
*
102+
* @memberof CodeSnippet
103+
*/
104+
@Input() feedbackText = this.translations.COPIED;
105+
106+
/**
107+
* Time in miliseconds to keep the feedback tooltip displayed.
108+
*
109+
* @memberof CodeSnippet
110+
*/
111+
@Input() feedbackTimeout = 2000;
112+
113+
@HostBinding("class.bx--snippet--expand") @Input() expanded = false;
114+
115+
@HostBinding("class.bx--snippet") snippetClass = true;
116+
@HostBinding("class.bx--snippet--single") get snippetSingleClass() {
117+
return this.display === SnippetType.single;
118+
}
119+
@HostBinding("class.bx--snippet--multi") get snippetMultiClass() {
120+
return this.display === SnippetType.multi;
121+
}
122+
@HostBinding("class.bx--snippet--inline") get snippetInlineClass() {
123+
return this.display === SnippetType.inline;
124+
}
125+
@HostBinding("class.bx--btn--copy") get btnCopyClass() {
126+
return this.display === SnippetType.inline;
127+
}
128+
129+
@HostBinding("style.display") get displayStyle() {
130+
return this.display !== SnippetType.inline ? "block" : null;
131+
}
132+
@HostBinding("attr.type") get attrType() {
133+
return this.display === SnippetType.inline ? "button" : null;
134+
}
135+
136+
@ViewChild("code") code;
137+
138+
get shouldShowExpandButton() {
139+
return this.code ? this.code.nativeElement.getBoundingClientRect().height > 255 : false;
140+
}
141+
142+
showFeedback = false;
143+
144+
/**
145+
* Creates an instance of CodeSnippet.
146+
* @param {ChangeDetectorRef} changeDetectorRef
147+
* @param {ElementRef} elementRef
148+
* @param {Renderer2} renderer
149+
* @memberof CodeSnippet
150+
*/
151+
constructor(protected i18n: I18n) {
152+
CodeSnippet.codeSnippetCount++;
153+
}
154+
155+
toggleSnippetExpansion() {
156+
this.expanded = !this.expanded;
157+
}
158+
159+
/**
160+
* Copies the code from the `<code>` block to clipboard.
161+
*
162+
* @memberof CodeSnippet
163+
*/
164+
copyCode() {
165+
// create invisible, uneditable textarea with our code in it
166+
const textarea = document.createElement("textarea");
167+
textarea.value = this.code.nativeElement.innerText || this.code.nativeElement.textContent;
168+
textarea.setAttribute("readonly", "");
169+
textarea.style.position = "absolute";
170+
textarea.style.right = "-99999px";
171+
document.body.appendChild(textarea);
172+
173+
// save user selection
174+
const selected = document.getSelection().rangeCount ? document.getSelection().getRangeAt(0) : null;
175+
176+
// copy to clipboard
177+
textarea.select();
178+
document.execCommand("copy");
179+
180+
// remove textarea
181+
document.body.removeChild(textarea);
182+
183+
// restore user selection
184+
if (selected) {
185+
document.getSelection().removeAllRanges();
186+
document.getSelection().addRange(selected);
187+
}
188+
}
189+
190+
onCopyButtonClicked() {
191+
this.copyCode();
192+
193+
this.showFeedback = true;
194+
195+
setTimeout(() => {
196+
this.showFeedback = false;
197+
}, this.feedbackTimeout);
198+
}
199+
200+
/**
201+
* Inline code snippet acts as button and makes the whole component clickable.
202+
*
203+
* This handles clicks in that case.
204+
*
205+
* @returns
206+
* @memberof CodeSnippet
207+
*/
208+
@HostListener("click")
209+
hostClick() {
210+
if (this.display !== SnippetType.inline) {
211+
return;
212+
}
213+
214+
this.onCopyButtonClicked();
215+
}
216+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// modules
2+
import { NgModule } from "@angular/core";
3+
import { FormsModule } from "@angular/forms";
4+
import { CommonModule } from "@angular/common";
5+
6+
import { I18nModule } from "../i18n/i18n.module";
7+
8+
// imports
9+
import { CodeSnippet } from "./code-snippet.component";
10+
11+
// exports
12+
export { CodeSnippet } from "./code-snippet.component";
13+
14+
@NgModule({
15+
declarations: [
16+
CodeSnippet
17+
],
18+
exports: [
19+
CodeSnippet
20+
],
21+
imports: [
22+
CommonModule,
23+
FormsModule,
24+
I18nModule
25+
]
26+
})
27+
export class CodeSnippetModule { }
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { storiesOf, moduleMetadata } from "@storybook/angular";
2+
import { withKnobs } from "@storybook/addon-knobs/angular";
3+
4+
import { CodeSnippetModule } from "..";
5+
6+
7+
const code = `import { storiesOf, moduleMetadata } from "@storybook/angular";
8+
import { withKnobs, boolean } from "@storybook/addon-knobs/angular";
9+
10+
import { CodeSnippetModule } from "..";
11+
12+
storiesOf("CodeSnippet", module).addDecorator(
13+
moduleMetadata({
14+
imports: [CodeSnippetModule]
15+
})
16+
)
17+
.addDecorator(withKnobs)
18+
.add("Basic", () => ({
19+
template: \`<ibm-code-snippet>code</ibm-code-snippet>\`,
20+
props: { // there's more
21+
// disabled: boolean("disabled", false)
22+
}
23+
}));`;
24+
25+
const lessCode = `import { storiesOf, moduleMetadata } from "@storybook/angular";
26+
import { withKnobs, boolean } from "@storybook/addon-knobs/angular";
27+
28+
import { CodeSnippetModule } from "..";
29+
30+
storiesOf("Code Snippet", module).addDecorator(
31+
moduleMetadata({
32+
imports: [CodeSnippetModule]
33+
})
34+
) // that's it, no more after this line
35+
`;
36+
37+
const inlineCode = "<inline code>";
38+
39+
storiesOf("CodeSnippet", module).addDecorator(
40+
moduleMetadata({
41+
imports: [CodeSnippetModule]
42+
})
43+
)
44+
.addDecorator(withKnobs)
45+
.add("Basic", () => ({
46+
template: `<ibm-code-snippet display="single">{{code}}</ibm-code-snippet>`,
47+
props: {
48+
code
49+
}
50+
}))
51+
.add("Multi", () => ({
52+
template: `
53+
<h2>With a lot of code</h2>
54+
<ibm-code-snippet display="multi">{{code}}</ibm-code-snippet>
55+
56+
<h2 style="margin-top: 60px">With less code</h2>
57+
<ibm-code-snippet display="multi">{{lessCode}}</ibm-code-snippet>
58+
`,
59+
props: {
60+
code,
61+
lessCode
62+
}
63+
}))
64+
.add("Inline", () => ({
65+
template: `Here is some <ibm-code-snippet display="inline">{{inlineCode}}</ibm-code-snippet> for you.`,
66+
props: {
67+
inlineCode
68+
}
69+
}));

src/combobox/combobox.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export class ComboBox implements OnChanges, OnInit, AfterViewInit, AfterContentI
243243
this.updatePills();
244244
this.propagateChangeCallback(this.view.getSelected());
245245
} else {
246-
if (event.item.selected) {
246+
if (event.item && event.item.selected) {
247247
this.selectedValue = event.item.content;
248248
this.propagateChangeCallback(event.item);
249249
} else {

src/common/utils.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,4 @@
1-
let _scrollbarWidth = -1;
2-
3-
export function getScrollbarWidth() {
4-
// lets not recreate this whole thing every time
5-
if (_scrollbarWidth >= 0) {
6-
return _scrollbarWidth;
7-
}
8-
9-
// do the calculations the first time
10-
const outer = document.createElement("div");
11-
outer.style.visibility = "hidden";
12-
outer.style.width = "100px";
13-
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
14-
15-
document.body.appendChild(outer);
16-
17-
const widthNoScroll = outer.offsetWidth;
18-
// force scrollbars
19-
outer.style.overflow = "scroll";
20-
21-
// add innerdiv
22-
const inner = document.createElement("div");
23-
inner.style.width = "100%";
24-
outer.appendChild(inner);
25-
26-
const widthWithScroll = inner.offsetWidth;
27-
28-
// remove divs
29-
outer.parentNode.removeChild(outer);
30-
31-
_scrollbarWidth = widthNoScroll - widthWithScroll;
32-
return _scrollbarWidth;
33-
}
1+
export * from "./../utils/window-tools";
342

353
/**
364
* Does what python's `range` function does, with a slightly different

0 commit comments

Comments
 (0)