Commit 1cfbf51a authored by Benjamin Franzke's avatar Benjamin Franzke
Browse files

[TASK] Streamline spinner web component size and animation

This change performs the following optimizations:

* Detach spinner from content flow using a relative+absolute
  position normalization, allowing an equivalent positioning
  to the regular backend icons.
  This is to ensure that both, <typo3-backend-spinner> and
  <typo3-backend-icon> render equally when used in inline(-block)
  containers (for example .svg-toolbar__drag-node).
  <typo3-backend-spinner> used to cause vertical alignment offsets,
  when the spinner was positioned in inline text-flow and therefore
  caused the vertical flow to cause offsets.

* Add a variant="light|dark" attribute to select
  between the available TYPO3 spinner variants.
  By default the current color is now used.

* Adapt sizing to inherit size from current font-size when used
  without a specific size attribute. Also apply that to the
  backend icon component to stay interchangeable.

* Render spinner via SVG and only animate the spinning part
  instead of the entire shape. This is to avoid the bouncing-icon
  effect that CSS animations on the entire element cause
  (as often seen with font-awesome spinners for example).

Releases: master
Resolves: #94149
Change-Id: I00d2e4915a0644726f78abe485fd9e276b539259
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/69162

Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
parent bce4af7c
......@@ -11,7 +11,6 @@
* The TYPO3 project - inspiring people to share!
*/
import module = require('module');
import {html, css, unsafeCSS, LitElement, TemplateResult, CSSResult} from 'lit';
import {customElement, property} from 'lit/decorators';
import {unsafeHTML} from 'lit/directives/unsafe-html';
......@@ -37,7 +36,7 @@ const iconSize = (identifier: CSSResult, size: number) => css`
@customElement('typo3-backend-icon')
export class IconElement extends LitElement {
@property({type: String}) identifier: string;
@property({type: String, reflect: true}) size: Sizes = Sizes.default;
@property({type: String}) size: Sizes = Sizes.default;
@property({type: String}) state: States = States.default;
@property({type: String}) overlay: string = null;
@property({type: String}) markup: MarkupIdentifiers = MarkupIdentifiers.inline;
......@@ -58,15 +57,11 @@ export class IconElement extends LitElement {
css`
:host {
display: flex;
font-size: 1em;
width: 1em;
height: 1em;
line-height: 0;
}
typo3-backend-spinner {
font-size: 1em;
}
.icon {
position: relative;
display: block;
......
......@@ -15,52 +15,79 @@ import {html, css, LitElement, TemplateResult} from 'lit';
import {customElement, property} from 'lit/decorators';
import {Sizes} from '../Enum/IconTypes';
enum Variant {
light = 'light',
dark = 'dark'
}
/**
* Module: TYPO3/CMS/Backend/Element/SpinnerElement
*
* @example
* <typo3-backend-spinner size="small"></typo3-backend-spinner>
* + attribute size can be one of small, medium, large
* <typo3-backend-spinner size="small" variant="dark"></typo3-backend-spinner>
* + attribute size can be one of small, default, large or mega
*/
@customElement('typo3-backend-spinner')
export class SpinnerElement extends LitElement {
@property({type: String}) size: Sizes = Sizes.default;
@property({type: String}) variant: Variant = Variant.dark;
static styles = css`
:host {
font-size: 32px;
display: flex;
width: 1em;
height: 1em;
display: flex;
justify-content: center;
align-items: center;
line-height: 0;
}
.icon {
position: relative;
display: block;
height: 1em;
width: 1em;
line-height: 1;
}
.spinner {
svg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
border-style: solid;
border-color: #212121 #bababa #bababa;
border-radius: 50%;
width: 0.625em;
height: 0.625em;
border-width: 0.0625em;
animation: spin 1s linear infinite;
height: 1em;
width: 1em;
transform: translate3d(0, 0, 0);
fill: currentColor;
}
:host([variant=dark]) svg {
fill: #212121;
}
:host([size=small]) .spinner {
:host([variant=light]) svg {
fill: #fff;
}
:host([size=small]) {
font-size: 16px;
}
:host([size=large]) .spinner {
:host([size=default]) {
font-size: 32px;
}
:host([size=large]) {
font-size: 48px;
}
:host([size=mega]) .spinner {
:host([size=mega]) {
font-size: 64px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
public render(): TemplateResult {
return html`<div class="spinner"></div>`
return html`
<div class="icon">
<svg viewBox="0 0 16 16">
<path d="M8 15c-3.86 0-7-3.141-7-7 0-3.86 3.14-7 7-7 3.859 0 7 3.14 7 7 0 3.859-3.141 7-7 7zM8 3C5.243 3 3 5.243 3 8s2.243 5 5 5 5-2.243 5-5 -2.243-5-5-5z" opacity=".3"/>
<path d="M14 9a1 1 0 0 1-1-1c0-2.757-2.243-5-5-5a1 1 0 0 1 0-2c3.859 0 7 3.14 7 7a1 1 0 0 1-1 1z" transform-origin="center center">
<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0" to="360" begin="0s" dur="1s" repeatCount="indefinite" />
</path>
</svg>
</div>
`;
}
}
......@@ -47,7 +47,7 @@ export class CodeMirrorElement extends LitElement {
return html`
<slot></slot>
<slot name="codemirror"></slot>
${this.loaded ? '' : html`<typo3-backend-spinner size="large"></typo3-backend-spinner>`}
${this.loaded ? '' : html`<typo3-backend-spinner size="large" variant="dark"></typo3-backend-spinner>`}
`;
}
......
......@@ -10,25 +10,21 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __decorate=this&&this.__decorate||function(e,t,i,n){var o,r=arguments.length,s=r<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(o=e[a])&&(s=(r<3?o(s):r>3?o(t,i,s):o(t,i))||s);return r>3&&s&&Object.defineProperty(t,i,s),s};define(["require","exports","lit","lit/decorators","lit/directives/unsafe-html","lit/directives/until","../Enum/IconTypes","../Icons","TYPO3/CMS/Backend/Element/SpinnerElement"],(function(e,t,i,n,o,r,s,a){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.IconElement=void 0;const l=(e,t)=>i.css`
var __decorate=this&&this.__decorate||function(e,t,i,o){var n,r=arguments.length,s=r<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,i,o);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(s=(r<3?n(s):r>3?n(t,i,s):n(t,i))||s);return r>3&&s&&Object.defineProperty(t,i,s),s};define(["require","exports","lit","lit/decorators","lit/directives/unsafe-html","lit/directives/until","../Enum/IconTypes","../Icons","TYPO3/CMS/Backend/Element/SpinnerElement"],(function(e,t,i,o,n,r,s,a){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.IconElement=void 0;const l=(e,t)=>i.css`
:host([size=${e}]),
:host([raw]) .icon-size-${e} {
font-size: ${t}px;
}
`;let c=class extends i.LitElement{constructor(){super(...arguments),this.size=s.Sizes.default,this.state=s.States.default,this.overlay=null,this.markup=s.MarkupIdentifiers.inline,this.raw=null}render(){if(this.raw)return i.html`${o.unsafeHTML(this.raw)}`;if(!this.identifier)return i.html``;const e=a.getIcon(this.identifier,this.size,this.overlay,this.state,this.markup).then(e=>i.html`
${o.unsafeHTML(e)}
`;let c=class extends i.LitElement{constructor(){super(...arguments),this.size=s.Sizes.default,this.state=s.States.default,this.overlay=null,this.markup=s.MarkupIdentifiers.inline,this.raw=null}render(){if(this.raw)return i.html`${n.unsafeHTML(this.raw)}`;if(!this.identifier)return i.html``;const e=a.getIcon(this.identifier,this.size,this.overlay,this.state,this.markup).then(e=>i.html`
${n.unsafeHTML(e)}
`);return i.html`${r.until(e,i.html`<typo3-backend-spinner></typo3-backend-spinner>`)}`}};c.styles=[i.css`
:host {
display: flex;
font-size: 1em;
width: 1em;
height: 1em;
line-height: 0;
}
typo3-backend-spinner {
font-size: 1em;
}
.icon {
position: relative;
display: block;
......@@ -98,4 +94,4 @@ var __decorate=this&&this.__decorate||function(e,t,i,n){var o,r=arguments.length
transform: rotate(360deg);
}
}
`,l(i.unsafeCSS(s.Sizes.small),16),l(i.unsafeCSS(s.Sizes.default),32),l(i.unsafeCSS(s.Sizes.large),48),l(i.unsafeCSS(s.Sizes.mega),64)],__decorate([n.property({type:String})],c.prototype,"identifier",void 0),__decorate([n.property({type:String,reflect:!0})],c.prototype,"size",void 0),__decorate([n.property({type:String})],c.prototype,"state",void 0),__decorate([n.property({type:String})],c.prototype,"overlay",void 0),__decorate([n.property({type:String})],c.prototype,"markup",void 0),__decorate([n.property({type:String})],c.prototype,"raw",void 0),c=__decorate([n.customElement("typo3-backend-icon")],c),t.IconElement=c}));
\ No newline at end of file
`,l(i.unsafeCSS(s.Sizes.small),16),l(i.unsafeCSS(s.Sizes.default),32),l(i.unsafeCSS(s.Sizes.large),48),l(i.unsafeCSS(s.Sizes.mega),64)],__decorate([o.property({type:String})],c.prototype,"identifier",void 0),__decorate([o.property({type:String})],c.prototype,"size",void 0),__decorate([o.property({type:String})],c.prototype,"state",void 0),__decorate([o.property({type:String})],c.prototype,"overlay",void 0),__decorate([o.property({type:String})],c.prototype,"markup",void 0),__decorate([o.property({type:String})],c.prototype,"raw",void 0),c=__decorate([o.customElement("typo3-backend-icon")],c),t.IconElement=c}));
\ No newline at end of file
......@@ -10,36 +10,57 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __decorate=this&&this.__decorate||function(e,t,r,n){var i,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,r,n);else for(var a=e.length-1;a>=0;a--)(i=e[a])&&(o=(s<3?i(o):s>3?i(t,r,o):i(t,r))||o);return s>3&&o&&Object.defineProperty(t,r,o),o};define(["require","exports","lit","lit/decorators","../Enum/IconTypes"],(function(e,t,r,n,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SpinnerElement=void 0;let s=class extends r.LitElement{constructor(){super(...arguments),this.size=i.Sizes.default}render(){return r.html`<div class="spinner"></div>`}};s.styles=r.css`
var __decorate=this&&this.__decorate||function(e,t,i,r){var o,n=arguments.length,s=n<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,i):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,i,r);else for(var a=e.length-1;a>=0;a--)(o=e[a])&&(s=(n<3?o(s):n>3?o(t,i,s):o(t,i))||s);return n>3&&s&&Object.defineProperty(t,i,s),s};define(["require","exports","lit","lit/decorators","../Enum/IconTypes"],(function(e,t,i,r,o){"use strict";var n;Object.defineProperty(t,"__esModule",{value:!0}),t.SpinnerElement=void 0,function(e){e.light="light",e.dark="dark"}(n||(n={}));let s=class extends i.LitElement{constructor(){super(...arguments),this.size=o.Sizes.default,this.variant=n.dark}render(){return i.html`
<div class="icon">
<svg viewBox="0 0 16 16">
<path d="M8 15c-3.86 0-7-3.141-7-7 0-3.86 3.14-7 7-7 3.859 0 7 3.14 7 7 0 3.859-3.141 7-7 7zM8 3C5.243 3 3 5.243 3 8s2.243 5 5 5 5-2.243 5-5 -2.243-5-5-5z" opacity=".3"/>
<path d="M14 9a1 1 0 0 1-1-1c0-2.757-2.243-5-5-5a1 1 0 0 1 0-2c3.859 0 7 3.14 7 7a1 1 0 0 1-1 1z" transform-origin="center center">
<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0" to="360" begin="0s" dur="1s" repeatCount="indefinite" />
</path>
</svg>
</div>
`}};s.styles=i.css`
:host {
font-size: 32px;
display: flex;
width: 1em;
height: 1em;
display: flex;
justify-content: center;
align-items: center;
line-height: 0;
}
.icon {
position: relative;
display: block;
height: 1em;
width: 1em;
line-height: 1;
}
.spinner {
svg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
border-style: solid;
border-color: #212121 #bababa #bababa;
border-radius: 50%;
width: 0.625em;
height: 0.625em;
border-width: 0.0625em;
animation: spin 1s linear infinite;
}
:host([size=small]) .spinner {
height: 1em;
width: 1em;
transform: translate3d(0, 0, 0);
fill: currentColor;
}
:host([variant=dark]) svg {
fill: #212121;
}
:host([variant=light]) svg {
fill: #fff;
}
:host([size=small]) {
font-size: 16px;
}
:host([size=large]) .spinner {
:host([size=default]) {
font-size: 32px;
}
:host([size=large]) {
font-size: 48px;
}
:host([size=mega]) .spinner {
:host([size=mega]) {
font-size: 64px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`,__decorate([n.property({type:String})],s.prototype,"size",void 0),s=__decorate([n.customElement("typo3-backend-spinner")],s),t.SpinnerElement=s}));
\ No newline at end of file
`,__decorate([r.property({type:String})],s.prototype,"size",void 0),__decorate([r.property({type:String})],s.prototype,"variant",void 0),s=__decorate([r.customElement("typo3-backend-spinner")],s),t.SpinnerElement=s}));
\ No newline at end of file
......@@ -13,7 +13,7 @@
var __decorate=this&&this.__decorate||function(e,t,o,r){var i,n=arguments.length,s=n<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,o,r);else for(var l=e.length-1;l>=0;l--)(i=e[l])&&(s=(n<3?i(s):n>3?i(t,o,s):i(t,o))||s);return n>3&&s&&Object.defineProperty(t,o,s),s},__importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","codemirror","lit","lit/decorators","TYPO3/CMS/Backend/FormEngine","TYPO3/CMS/Backend/Element/SpinnerElement"],(function(e,t,o,r,i,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CodeMirrorElement=void 0,o=__importDefault(o);let s=class extends r.LitElement{constructor(){super(...arguments),this.addons=[],this.options={},this.loaded=!1}render(){return r.html`
<slot></slot>
<slot name="codemirror"></slot>
${this.loaded?"":r.html`<typo3-backend-spinner size="large"></typo3-backend-spinner>`}
${this.loaded?"":r.html`<typo3-backend-spinner size="large" variant="dark"></typo3-backend-spinner>`}
`}firstUpdated(){const e={root:document.body};let t=new IntersectionObserver(e=>{e.forEach(e=>{e.intersectionRatio>0&&(t.unobserve(e.target),this.firstElementChild&&"textarea"===this.firstElementChild.nodeName.toLowerCase()&&this.initializeEditor(this.firstElementChild))})},e);t.observe(this)}createPanelNode(e,t){const o=document.createElement("div");o.setAttribute("class","CodeMirror-panel CodeMirror-panel-"+e),o.setAttribute("id","panel-"+e);const r=document.createElement("span");return r.textContent=t,o.appendChild(r),o}initializeEditor(t){const r=this.mode.split("/"),i=this.options;e([this.mode,...this.addons],()=>{const e=o.default(e=>{const o=document.createElement("div");o.setAttribute("slot","codemirror"),o.appendChild(e),this.insertBefore(o,t)},{value:t.value,extraKeys:{"Ctrl-F":"findPersistent","Cmd-F":"findPersistent","Ctrl-Alt-F":e=>{e.setOption("fullScreen",!e.getOption("fullScreen"))},"Ctrl-Space":"autocomplete",Esc:e=>{e.getOption("fullScreen")&&e.setOption("fullScreen",!1)}},fullScreen:!1,lineNumbers:!0,lineWrapping:!0,mode:r[r.length-1]});Object.keys(i).map(t=>{e.setOption(t,i[t])}),e.on("change",()=>{t.value=e.getValue(),n.Validation.markFieldAsChanged(t)});const s=this.createPanelNode("bottom",this.label);if(e.addPanel(s,{position:"bottom",stable:!1}),t.getAttribute("rows")){const o=18,r=4;e.setSize(null,parseInt(t.getAttribute("rows"),10)*o+r+s.getBoundingClientRect().height)}else e.getWrapperElement().style.height=document.body.getBoundingClientRect().height-e.getWrapperElement().getBoundingClientRect().top-80+"px",e.setOption("viewportMargin",1/0);this.loaded=!0})}};s.styles=r.css`
:host {
display: block;
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment