Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions DotSpinner/DotSpinner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Provides a Limestone-themed three-dot indeterminate progress indicator.
*
* @example
* <DotSpinner>Loading...</DotSpinner>
*
* @module limestone/DotSpinner
* @exports DotSpinner
* @exports DotSpinnerBase
* @exports DotSpinnerDecorator
*/
import kind from '@enact/core/kind';
import Pure from '@enact/ui/internal/Pure';
import PropTypes from 'prop-types';
import compose from 'ramda/src/compose';

import Marquee from '../Marquee';
import Skinnable from '../Skinnable';

import componentCss from './DotSpinner.module.less';

/**
* A component that renders three animated dots.
*
* @class DotSpinnerBase
* @memberof limestone/DotSpinner
* @ui
* @public
*/
const DotSpinnerBase = kind({
name: 'DotSpinner',

propTypes: /** @lends limestone/DotSpinner.DotSpinnerBase.prototype */ {
/**
* Customizes the component by mapping the supplied collection of CSS class names to the
* corresponding internal elements and states of this component.
*
* The following classes are supported:
*
* * `dotSpinner` - The root component class
*
* @type {Object}
* @public
*/
css: PropTypes.object,

/**
* Pauses the animation.
*
* @type {Boolean}
* @default false
* @public
*/
paused: PropTypes.bool,

/**
* The size of the spinner.
*
* @type {('medium'|'small')}
* @default 'medium'
* @public
*/
size: PropTypes.oneOf(['medium', 'small']),

/**
* Removes the background color (making it transparent).
*
* @type {Boolean}
* @default false
* @public
*/
transparent: PropTypes.bool
},

defaultProps: {
paused: false,
size: 'medium',
transparent: false
},

styles: {
css: componentCss,
className: 'dotSpinner',
publicClassNames: ['dotSpinner']
},

computed: {
'aria-label': ({['aria-label']: aria, children}) => aria || (!children ? 'Loading' : null),
className: ({children, paused, size, transparent, styler}) =>
styler.append(size, {content: !!children, paused, transparent})
},

render: ({children, css, ...rest}) => {
delete rest.paused;
delete rest.size;
delete rest.transparent;

return (
<div aria-live="off" role="alert" {...rest}>
<div className={css.dots}>
<div className={css.dot} />
<div className={css.dot} />
<div className={css.dot} />
</div>
{children ?
<Marquee className={css.client} marqueeOn="render" alignment="center">
{children}
</Marquee> :
null
}
</div>
);
}
});

/**
* Limestone-specific DotSpinner behaviors.
*
* @hoc
* @memberof limestone/DotSpinner
* @mixes limestone/Skinnable.Skinnable
* @public
*/
const DotSpinnerDecorator = compose(
Pure,
Skinnable
);

/**
* A Limestone-styled three-dot Spinner.
*
* @class DotSpinner
* @memberof limestone/DotSpinner
* @extends limestone/DotSpinner.DotSpinnerBase
* @mixes limestone/DotSpinner.DotSpinnerDecorator
* @ui
* @public
*/
const DotSpinner = DotSpinnerDecorator(DotSpinnerBase);

export default DotSpinner;
export {
DotSpinner,
DotSpinnerBase,
DotSpinnerDecorator
};
203 changes: 203 additions & 0 deletions DotSpinner/DotSpinner.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// DotSpinner.module.less
//
@import "../styles/mixins.less";
@import "../styles/variables.less";
@import "../styles/skin.less";

@dot-spinner-duration: 4s;
@dot-spinner-dot-size: 20px;
@dot-spinner-gap: 12px;

@keyframes dot-show-second {
0% { opacity: 0; }
9% { opacity: 0; }
17% { opacity: 1; }
91% { opacity: 1; }
99% { opacity: 0; }
100% { opacity: 0; }
}

@keyframes dot-show-third {
0% { opacity: 0; }
17% { opacity: 0; }
25% { opacity: 1; }
75% { opacity: 1; }
83% { opacity: 0; }
100% { opacity: 0; }
}

@keyframes dot-show-first {
0% { opacity: 0; }
1% { opacity: 0; }
9% { opacity: 1; }
83% { opacity: 1; }
91% { opacity: 0; }
100% { opacity: 0; }
}

@keyframes dot-rotate-first {
0% {
transform: rotate(0deg) translateX(0);
transform-origin: 26px center;
}
25% {
transform: rotate(0deg) translateX(0);
transform-origin: 26px center;
}
37.5% {
transform: rotate(180deg) translateX(0);
transform-origin: 26px center;
}
37.51% {
transform: rotate(0deg) translateX(32px);
transform-origin: 58px center;
}
50% {
transform: rotate(-180deg) translate(32px);
transform-origin: 58px center;
}
62.5% {
transform: rotate(-180deg) translate(32px);
transform-origin: 58px center;
}
75% {
transform: rotate(-360deg) translateX(32px);
transform-origin: 58px center;
}
100% {
transform: rotate(-360deg) translate(32px);
transform-origin: 58px center;
}
}

@keyframes dot-rotate-second {
0% {
transform: rotate(0turn) translateX(0);
transform-origin: -6px center;
}
25% {
transform: rotate(0turn) translateX(0);
transform-origin: -6px center;
}
37.5% {
transform: rotate(180deg) translateX(0);
transform-origin: -6px center;
}
50% {
transform: rotate(180deg) translateX(0);
transform-origin: -6px center;
}
62.5% {
transform: rotate(360deg) translateX(0);
transform-origin: -6px center;
}
62.51% {
transform: rotate(0deg) translateX(0);
transform-origin: 26px center;
}
75% {
transform: rotate(-180deg) translateX(0);
transform-origin: 26px center;
}
100% {
transform: rotate(-180deg) translate(0);
transform-origin: 26px center;
}
}

@keyframes dot-rotate-third {
0% {
transform: rotate(0turn) translateX(0);
transform-origin: -6px center;
}
37.5% {
transform: rotate(0turn) translateX(0);
transform-origin: -6px center;
}
50% {
transform: rotate(-180deg) translateX(0);
transform-origin: -6px center;
}
50.01% {
transform: rotate(0deg) translateX(-32px);
transform-origin: -38px center;
}
62.5% {
transform: rotate(180deg) translateX(-32px);
transform-origin: -38px center;
}
100% {
transform: rotate(180deg) translateX(-32px);
transform-origin: -38px center;
}
}

.dotSpinner {
display: inline-flex;
flex-direction: column;
align-items: center;
vertical-align: middle;

.dots {
display: flex;
flex-direction: row;
align-items: center;
gap: @dot-spinner-gap;
}

.dot {
width: @dot-spinner-dot-size;
height: @dot-spinner-dot-size;
border-radius: 50%;
background-color: var(--semantic-color-on-background-main, #ffffff);

&:nth-child(1) {
animation:
dot-show-first @dot-spinner-duration ease-in-out infinite,
dot-rotate-first @dot-spinner-duration ease-in-out infinite;
// dot-hide-first @dot-spinner-duration linear infinite;
animation-play-state: running;
}
&:nth-child(2) {
// transform-origin: -6px center;
animation:
dot-show-second @dot-spinner-duration linear infinite,
dot-rotate-second @dot-spinner-duration ease-in-out infinite;
animation-play-state: running;
}
&:nth-child(3) {
// transform-origin: -6px center;
animation:
dot-show-third @dot-spinner-duration linear infinite,
dot-rotate-third @dot-spinner-duration ease-in-out infinite;
animation-play-state: running;
}
}

&.paused .dot {
animation-play-state: paused;
}

.client {
.lime-body-text();
align-content: center;
height: @lime-spinner-client-height;
font-weight: @lime-spinner-font-weight;
line-height: @lime-spinner-client-line-height;
max-width: 696px;
margin-top: 16px;
}

// Skin colors
.applySkins({
color: @lime-spinner-label-color;

.dot {
background-color: @lime-spinner-color;
}

&.transparent .dot {
background-color: @lime-spinner-color;
}
});
}
3 changes: 3 additions & 0 deletions DotSpinner/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"main": "DotSpinner.js"
}
Loading
Loading