Skip to content

Commit b418fb3

Browse files
authored
Add react icon component (#124)
* Improve icon element DOM tree * Add icon style * Fix styles for slots with icon * Export the icon * Add the react icon component * Allow to register icons using the react component * Fix tests * Fix format * Sanitize icon SVG strings * Update button icon snapshots --------- Co-authored-by: Frédéric Collonval <[email protected]>
1 parent 1583278 commit b418fb3

File tree

68 files changed

+255
-61
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+255
-61
lines changed

.github/workflows/build.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ jobs:
4848
yarn-
4949
5050
- name: Install dependencies
51-
run: python -m pip install -U jupyterlab~=4.0
51+
# Use JLab 4.0.x branch because otherwise this will not bring
52+
# the current toolkit version but the core version one.
53+
run: python -m pip install -U jupyterlab~=4.0.0
5254
- name: Build the extension
5355
run: |
5456
set -eux
@@ -94,6 +96,6 @@ jobs:
9496
sudo rm -rf $(which node)
9597
sudo rm -rf $(which node)
9698
pip install myextension.tar.gz
97-
pip install jupyterlab
99+
pip install "jupyterlab~=4.0.0"
98100
jupyter labextension list 2>&1 | grep -ie "jupyter-ui-demo.*enabled"
99101
python -m jupyterlab.browser_check --no-browser-test

binder/environment.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ channels:
1212
dependencies:
1313
# runtime dependencies
1414
- python >=3.10,<3.11.0a0
15-
- jupyterlab >=4.0.0,<5
15+
- jupyterlab >=4.0.0,<4.1.0
1616
# labextension build dependencies
17-
- nodejs >=18,<19
17+
- nodejs >=20,<21
1818
- pip
1919
- wheel

packages/components/src/breadcrumb-item/breadcrumb-item.styles.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ export const breadcrumbItemStyles: FoundationElementTemplate<
119119
display: flex;
120120
}
121121
122-
::slotted(svg) {
122+
::slotted(svg),
123+
::slotted(jp-icon) {
123124
/* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */
124125
width: 16px;
125126
height: 16px;

packages/components/src/button/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { attr } from '@microsoft/fast-element';
66
import { Button, buttonTemplate as template } from '@microsoft/fast-foundation';
77
import { buttonStyles as styles } from './button.styles.js';
8+
import { Icon } from '../icon/index.js';
89

910
/**
1011
* Scale locally an element.
@@ -73,9 +74,8 @@ class JupyterButton extends Button {
7374
);
7475
if (
7576
slottedElements.length === 1 &&
76-
(slottedElements[0] instanceof SVGElement ||
77-
slottedElements[0].classList.contains('fa') ||
78-
slottedElements[0].classList.contains('fas'))
77+
(slottedElements[0] instanceof Icon ||
78+
slottedElements[0] instanceof SVGElement)
7979
) {
8080
this.control.classList.add('icon-only');
8181
} else {

packages/components/src/custom-elements.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { DesignSystemProvider } from './design-system-provider/index.js';
2424
import type { Dialog } from './dialog/index.js';
2525
import type { Disclosure } from './disclosure/index.js';
2626
import type { Divider } from './divider/index.js';
27+
import type { Icon } from './icon/index.js';
2728
import type { Listbox } from './listbox/index.js';
2829
import type { Menu } from './menu/index.js';
2930
import type { MenuItem } from './menu-item/index.js';

packages/components/src/icon/icon.stories.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { Icon } from './index';
44
// Register the icon with proper SVG formatting
55
Icon.register({
66
name: 'search',
7-
svgStr: `
8-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
9-
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" fill="currentColor"/>
7+
svgStr: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
8+
<path
9+
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
10+
fill="currentColor"
11+
/>
1012
</svg>`
1113
});
1214

@@ -25,8 +27,12 @@ const Template: StoryFn = (args, context): string => {
2527
setTimeout(() => {
2628
Icon.register({
2729
name: 'search',
28-
svgStr: `
29-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 576 512"><path d="M575.8 255.5c0 18-15 32.1-32 32.1l-32 0 .7 160.2c0 2.7-.2 5.4-.5 8.1l0 16.2c0 22.1-17.9 40-40 40l-16 0c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1L416 512l-24 0c-22.1 0-40-17.9-40-40l0-24 0-64c0-17.7-14.3-32-32-32l-64 0c-17.7 0-32 14.3-32 32l0 64 0 24c0 22.1-17.9 40-40 40l-24 0-31.9 0c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2l-16 0c-22.1 0-40-17.9-40-40l0-112c0-.9 0-1.9 .1-2.8l0-69.7-32 0c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z" fill="currentColor"/></svg>`
30+
svgStr: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 576 512">
31+
<path
32+
d="M575.8 255.5c0 18-15 32.1-32 32.1l-32 0 .7 160.2c0 2.7-.2 5.4-.5 8.1l0 16.2c0 22.1-17.9 40-40 40l-16 0c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1L416 512l-24 0c-22.1 0-40-17.9-40-40l0-24 0-64c0-17.7-14.3-32-32-32l-64 0c-17.7 0-32 14.3-32 32l0 64 0 24c0 22.1-17.9 40-40 40l-24 0-31.9 0c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2l-16 0c-22.1 0-40-17.9-40-40l0-112c0-.9 0-1.9 .1-2.8l0-69.7-32 0c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
33+
fill="currentColor"
34+
/>
35+
</svg>`
3036
});
3137
}, args.delay);
3238
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import { css, ElementStyles } from '@microsoft/fast-element';
5+
import { display } from '@microsoft/fast-foundation';
6+
import { heightNumber } from '../styles/index.js';
7+
8+
/**
9+
* Styles for Badge
10+
* @public
11+
*/
12+
export const iconStyles: ElementStyles = css`
13+
${display('inline-block')} :host {
14+
inline-size: ${heightNumber};
15+
block-size: ${heightNumber};
16+
}
17+
18+
:host svg {
19+
display: block;
20+
width: 100%;
21+
height: 100%;
22+
}
23+
`;

packages/components/src/icon/index.ts

+51-20
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import {
2-
FASTElement,
3-
customElement,
4-
attr,
5-
html
6-
} from '@microsoft/fast-element';
1+
import { DOM, FASTElement, customElement, attr } from '@microsoft/fast-element';
2+
import { iconStyles as styles } from './icon.styles';
73

8-
const template = html<Icon>`<div :innerHTML="${x => x.getSvg()}"></div>`;
4+
/**
5+
* Icon definition
6+
*/
7+
export interface IconDefinition {
8+
/**
9+
* Icon unique name
10+
*/
11+
name: string;
12+
/**
13+
* Icon SVG as string
14+
*/
15+
svgStr: string;
16+
}
917

1018
/**
1119
* Icon component
@@ -19,24 +27,24 @@ const template = html<Icon>`<div :innerHTML="${x => x.getSvg()}"></div>`;
1927
*/
2028
@customElement({
2129
name: 'jp-icon',
22-
template
30+
styles
2331
})
2432
export class Icon extends FASTElement {
25-
/**
26-
* Name of the icon to display.
27-
*/
28-
@attr name: string;
29-
3033
private static iconsMap = new Map<string, string>();
3134
private static _defaultIcon =
32-
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" fill="currentColor"/></svg>';
35+
DOM.createHTML(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16">
36+
<path
37+
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
38+
fill="currentColor"
39+
/>
40+
</svg>`);
3341

3442
/**
3543
* Register a new icon.
3644
*
37-
* @param options { name: Icon unique name, svgStr: Icon SVG as string }
45+
* @param options Icon definition
3846
*/
39-
static register(options: { name: string; svgStr: string }): void {
47+
static register(options: IconDefinition): void {
4048
if (Icon.iconsMap.has(options.name)) {
4149
console.warn(
4250
`Redefining previously loaded icon svgStr. name: ${
@@ -46,19 +54,19 @@ export class Icon extends FASTElement {
4654
}`
4755
);
4856
}
49-
Icon.iconsMap.set(options.name, options.svgStr);
57+
Icon.iconsMap.set(options.name, DOM.createHTML(options.svgStr));
5058

5159
// Rerender all existing icons with the same name
5260
document
53-
.querySelectorAll(`jp-icon[name="${options.name}"]`)
61+
?.querySelectorAll(`jp-icon[name="${options.name}"]`)
5462
.forEach((node: HTMLElement) => {
5563
node.setAttribute('name', '');
5664
node.setAttribute('name', options.name);
5765
});
5866
}
5967

6068
/**
61-
* Set a new default icon.
69+
* Set the default icon.
6270
*
6371
* @param svgStr The SVG string to be used as the default icon.
6472
*/
@@ -76,10 +84,33 @@ export class Icon extends FASTElement {
7684
return Icon._defaultIcon;
7785
}
7886

87+
/**
88+
* Name of the icon to display.
89+
*/
90+
@attr name: string;
91+
nameChanged() {
92+
if (this.shadowRoot) {
93+
this.shadowRoot.innerHTML = this.getSvg();
94+
}
95+
}
96+
97+
/**
98+
* The connected callback for this FASTElement.
99+
* @remarks
100+
* This method is invoked by the platform whenever this FASTElement
101+
* becomes connected to the document.
102+
*/
103+
connectedCallback(): void {
104+
super.connectedCallback();
105+
this.nameChanged();
106+
}
107+
79108
/**
80109
* Get the icon SVG
81110
*/
82-
getSvg(): string {
111+
protected getSvg(): string {
83112
return Icon.iconsMap.get(this.name) ?? Icon.defaultIcon();
84113
}
85114
}
115+
116+
export { styles as iconStyles };

packages/components/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export * from './design-tokens.js';
3636
export * from './dialog/index.js';
3737
export * from './disclosure/index.js';
3838
export * from './divider/index.js';
39+
export * from './icon/index.js';
3940
export * from './listbox/index.js';
4041
export * from './menu/index.js';
4142
export * from './menu-item/index.js';

packages/components/src/menu-item/menu-item.styles.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ export const menuItemStyles: FoundationElementTemplate<
144144
145145
:host([disabled]:hover) .start,
146146
:host([disabled]:hover) .end,
147-
:host([disabled]:hover)::slotted(svg) {
147+
:host([disabled]:hover)::slotted(svg),
148+
:host([disabled]:hover)::slotted(jp-icon) {
148149
fill: ${neutralForegroundRest};
149150
}
150151
@@ -168,7 +169,8 @@ export const menuItemStyles: FoundationElementTemplate<
168169
justify-content: center;
169170
}
170171
171-
::slotted(svg) {
172+
::slotted(svg),
173+
::slotted(jp-icon) {
172174
/* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */
173175
width: 16px;
174176
height: 16px;
@@ -183,9 +185,11 @@ export const menuItemStyles: FoundationElementTemplate<
183185
:host(:hover) .start,
184186
:host(:hover) .end,
185187
:host(:hover)::slotted(svg),
188+
:host(:hover)::slotted(jp-icon),
186189
:host(:active) .start,
187190
:host(:active) .end,
188-
:host(:active)::slotted(svg) {
191+
:host(:active)::slotted(svg),
192+
:host(:active)::slotted(jp-icon) {
189193
fill: ${neutralForegroundRest};
190194
}
191195
@@ -331,9 +335,11 @@ export const menuItemStyles: FoundationElementTemplate<
331335
:host(:hover) .start,
332336
:host(:hover) .end,
333337
:host(:hover)::slotted(svg),
338+
:host(:hover)::slotted(jp-icon),
334339
:host(:active) .start,
335340
:host(:active) .end,
336-
:host(:active)::slotted(svg) {
341+
:host(:active)::slotted(svg),
342+
:host(:active)::slotted(jp-icon) {
337343
fill: ${SystemColors.HighlightText};
338344
}
339345
@@ -356,7 +362,8 @@ export const menuItemStyles: FoundationElementTemplate<
356362
:host([disabled]:hover),
357363
:host([disabled]:hover) .start,
358364
:host([disabled]:hover) .end,
359-
:host([disabled]:hover)::slotted(svg) {
365+
:host([disabled]:hover)::slotted(svg),
366+
:host([disabled]:hover)::slotted(jp-icon) {
360367
background: ${SystemColors.Canvas};
361368
color: ${SystemColors.GrayText};
362369
fill: currentcolor;

packages/components/src/option/option.styles.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,13 @@ export const optionStyles: FoundationElementTemplate<
110110
111111
.start,
112112
.end,
113-
::slotted(svg) {
113+
::slotted(svg),
114+
::slotted(jp-icon) {
114115
display: flex;
115116
}
116117
117-
::slotted(svg) {
118+
::slotted(svg),
119+
::slotted(jp-icon) {
118120
/* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */
119121
height: calc(${designUnit} * 4px);
120122
width: calc(${designUnit} * 4px);

packages/components/src/search/search.styles.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ export const searchStyles: FoundationElementTemplate<
115115
height: calc(100% - 2px);
116116
}
117117
118-
::slotted(svg) {
118+
::slotted(svg),
119+
::slotted(jp-icon) {
119120
/* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */
120121
width: 16px;
121122
height: 16px;

packages/components/src/select/select.styles.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,8 @@ export const selectStyles: FoundationElementTemplate<
281281
.end,
282282
.indicator,
283283
.select-indicator,
284-
::slotted(svg) {
284+
::slotted(svg),
285+
::slotted(jp-icon) {
285286
/* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */
286287
fill: currentcolor;
287288
height: 1em;
@@ -355,7 +356,8 @@ export const selectStyles: FoundationElementTemplate<
355356
.end,
356357
.indicator,
357358
.select-indicator,
358-
::slotted(svg) {
359+
::slotted(svg),
360+
::slotted(jp-icon) {
359361
color: ${SystemColors.ButtonText};
360362
fill: currentcolor;
361363
}

packages/components/src/skeleton/skeleton.styles.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export const skeletonStyles: FoundationElementTemplate<ElementStyles> = (
7676
z-index: 1;
7777
}
7878
79-
::slotted(svg) {
79+
::slotted(svg),
80+
::slotted(jp-icon) {
8081
z-index: 2;
8182
}
8283

packages/components/src/styles/patterns/button.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ export const BaseButtonStyles = css`
148148
line-height: 0;
149149
}
150150
151-
::slotted(svg) {
151+
::slotted(svg),
152+
::slotted(jp-icon) {
152153
${
153154
/* Glyph size and margin-left is temporary -
154155
replace when adaptive typography is figured out */ ''

packages/components/src/styles/patterns/field.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ export const BaseFieldStyles = css`
108108
fill: currentcolor;
109109
}
110110
111-
::slotted(svg) {
111+
::slotted(svg),
112+
::slotted(jp-icon) {
112113
/* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */
113114
width: 16px;
114115
height: 16px;

packages/components/src/toolbar/toolbar.styles.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ export const toolbarStyles: FoundationElementTemplate<
7272
margin-inline: 0;
7373
}
7474
75-
::slotted(svg) {
75+
::slotted(svg),
76+
::slotted(jp-icon) {
7677
/* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */
7778
width: 16px;
7879
height: 16px;

0 commit comments

Comments
 (0)