Progress Bar

Line Box Circle Fill Circle

Line

Something here

42%

Something here 2

63%

Something here 3

88%

Box

67%

Something here

Circle Fill

Something here

67%

Circle

Something here

67%

block.json

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wlc/progress-bar",
  "title": "Progress Bar",
  "category": "widgets",
  "icon": "admin-generic",
  "description": "A block to display progress bar",
  "keywords": [
    "progress",
    "bar"
  ],
  "supports": {
    "interactivity": true
  },
  "attributes": {
    "type": {
      "type": "string",
      "default": "line"
    },
    "title": {
      "type": "string"
    },
    "counterValue": {
      "type": "number",
      "default": 60
    },
    "showCounter": {
      "type": "boolean",
      "default": false
    },
    "width": {
      "type": "number",
      "default": 300
    },
    "height": {
      "type": "number"
    },
    "circleBorderWidth": {
      "type": "number",
      "default": 15
    },
    "titleColor": {
      "type": "string",
      "default": "#000"
    },
    "counterTextColor": {
      "type": "string",
      "default": "#000"
    },
    "background": {
      "type": "string",
      "default": "#F4F4F4"
    },
    "fill": {
      "type": "string",
      "default": "#75E2C2"
    },
    "animate": {
      "type": "boolean",
      "default": false
    }
  },
  "version": "1.0.0",
  "textdomain": "WLC",
  "editorStyle": "file:./index.css",
  "editorScript": "file:./index.js",
  "viewScriptModule": "file:/view.js",
  "style": "file:./style-index.css"
}

edit.js

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import {
  useBlockProps,
  InspectorControls,
  RichText,
} from '@wordpress/block-editor';
import {
  PanelBody,
  SelectControl,
  ToggleControl,
  ColorPicker,
  RangeControl,
} from '@wordpress/components';

const edit = ( { attributes, setAttributes } ) => {
  const { type, title, counterValue, showCounter, width, height, circleBorderWidth, titleColor, counterTextColor, background, fill, animate } = attributes;
  const blockProps = useBlockProps();

  const circleRightRotation = counterValue <= 50 ? ( counterValue / 50 ) * 180 : 180;
  const circleLeftRotation = counterValue > 50 ? 180 + ( ( counterValue - 50 ) / 50 ) * 180 : 0;

  return (
    <div { ...blockProps }>
      <InspectorControls>
        <PanelBody title={ __( 'Progress Bar Settings', 'WLC' ) }>
          <SelectControl
            label={ __( 'Type', 'WLC' ) }
            value={ type }
            options={ [
              { label: __( 'Line', 'WLC' ), value: 'line' },
              { label: __( 'Circle', 'WLC' ), value: 'circle' },
              { label: __( 'Circle Fill', 'WLC' ), value: 'circle-fill' },
              { label: __( 'Box', 'WLC' ), value: 'box' },
            ] }
            onChange={ value => setAttributes( { type: value } ) }
          />
          <RangeControl
            label={ __( 'Counter Value (%)', 'WLC' ) }
            value={ counterValue }
            onChange={ ( value ) => setAttributes( { counterValue: value } ) }
            min={ 0 }
            max={ 100 }
          />
          <ToggleControl
            label={ __( 'Show Counter', 'WLC' ) }
            checked={ showCounter }
            onChange={ value => setAttributes( { showCounter: value } ) }
          />
          <ToggleControl
            label={ __( 'Animate', 'WLC' ) }
            checked={ animate }
            onChange={ value => setAttributes( { animate: value } ) }
          />
          { ( type !== 'line' ) && (
            <RangeControl
              label={ __( 'Width (px)', 'WLC' ) }
              value={ width }
              onChange={ ( value ) => setAttributes( { width: value } ) }
              min={ 0 }
              max={ 500 }
            />
          ) }
          { ( type == 'line' || type == 'box' ) && (
            <RangeControl
              label={ __( 'Height (px)', 'WLC' ) }
              value={ height }
              initialPosition={ type == 'line' ? 15 : 300 }
              onChange={ ( value ) => setAttributes( { height: value } ) }
              min={ 0 }
              max={ type == 'line' ? 50 : 500 }
            />
          ) }
          { ( type == 'circle' ) && (
            <RangeControl
              label={ __( 'Border Width (px)', 'WLC' ) }
              value={ circleBorderWidth }
              onChange={ ( value ) => setAttributes( { circleBorderWidth: value } ) }
              min={ 1 }
              max={ 50 }
            />
          ) }
        </PanelBody>
        <PanelBody title={ __( 'Colors', 'WLC' ) } initialOpen={ false }>
          <div className="components-base-control">
            <div className="components-base-control__field">
              <label className="components-base-control__label">
                { __( 'Title Color', 'WLC' ) }
              </label>
              <ColorPicker
                color={ titleColor }
                onChangeComplete={ ( value ) => setAttributes( { titleColor: value.hex } ) }
              />
            </div>
          </div>
          { showCounter && (
            <div className="components-base-control">
              <div className="components-base-control__field">
                <label className="components-base-control__label">
                  { __( 'Counter Text Color', 'WLC' ) }
                </label>
                <ColorPicker
                  color={ counterTextColor }
                  onChangeComplete={ ( value ) => setAttributes( { counterTextColor: value.hex } ) }
                />
              </div>
            </div>
          ) }
          <div className="components-base-control">
            <div className="components-base-control__field">
              <label className="components-base-control__label">
                { __( 'Background', 'WLC' ) }
              </label>
              <ColorPicker
                color={ background }
                onChangeComplete={ ( value ) => setAttributes( { background: value.hex } ) }
              />
            </div>
          </div>
          <div className="components-base-control">
            <div className="components-base-control__field">
              <label className="components-base-control__label">
                { __( 'Fill', 'WLC' ) }
              </label>
              <ColorPicker
                color={ fill }
                onChangeComplete={ ( value ) => setAttributes( { fill: value.hex } ) }
              />
            </div>
          </div>
        </PanelBody>
      </InspectorControls>
      <div { ...blockProps }>
        { type == 'line' && (
          <div className="progressbar-line">
            <div className="progressbar-line__header">
              <RichText
                tagName="p"
                placeholder={ __( 'Type title...', 'WLC' ) }
                className="progressbar-line__title"
                value={ title }
                onChange={ ( value ) => setAttributes( { title: value } ) }
                style={ { color: titleColor } }
              />
              { showCounter && (
                <p className="progressbar-line__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
              ) }
            </div>
            <div className="progressbar-line__inner" style={ { height: `${height}px`, backgroundColor: background } }>
              <div className="progressbar-line__fill" style={ { backgroundColor: fill, width: counterValue + `%` } }></div>
            </div>
          </div>
        ) }
        { ( type == 'circle' ) && (
          <div className="progressbar-circle" style={ { width: `${width}px`, height: `${width}px` } }>
            <div className="progressbar-circle__fill" style={ counterValue > 50 ? { clipPath: 'inset(0px)' } : {} }>
              <div
                className="progressbar-circle__fill-left"
                style={ { borderColor: fill, borderWidth: circleBorderWidth, transform: `rotate(${circleLeftRotation}deg)` } }
              ></div>
              <div
                className="progressbar-circle__fill-right"
                style={ { borderColor: fill, borderWidth: circleBorderWidth, transform: `rotate(${circleRightRotation}deg)` } }
              ></div>
            </div>
            <div className="progressbar-circle__shape" style={ { borderColor: background, borderWidth: circleBorderWidth } }></div>
            <div className="progressbar-circle__content">
              <RichText
                tagName="p"
                placeholder={ __( 'Type title...', 'WLC' ) }
                className="progressbar-circle__title"
                value={ title }
                onChange={ ( value ) => setAttributes( { title: value } ) }
                style={ { color: titleColor } }
              />
              { showCounter && (
                <p className="progressbar-circle__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
              ) }
            </div>
          </div>
        ) }
        { ( type == 'circle-fill' ) && (
          <div className="progressbar-circle circle-fill" style={ { width: `${width}px`, height: `${width}px` } }>
            <div className="progressbar-circle__fill" style={ counterValue > 50 ? { clipPath: 'inset(0px)' } : {} }>
              <div
                className="progressbar-circle__fill-left"
                style={ { backgroundColor: fill, transform: `rotate(${circleLeftRotation}deg)` } }
              ></div>
              <div
                className="progressbar-circle__fill-right"
                style={ { backgroundColor: fill, transform: `rotate(${circleRightRotation}deg)` } }
              ></div>
            </div>
            <div className="progressbar-circle__shape" style={ { backgroundColor: background } }></div>
            <div className="progressbar-circle__content">
              <RichText
                tagName="p"
                placeholder={ __( 'Type title...', 'WLC' ) }
                className="progressbar-circle__title"
                value={ title }
                onChange={ ( value ) => setAttributes( { title: value } ) }
                style={ { color: titleColor } }
              />
              { showCounter && (
                <p className="progressbar-circle__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
              ) }
            </div>
          </div>
        ) }
        { type == 'box' && (
          <div className="progressbar-box">
            <div className="progressbar-box__inner" style={ { width: `${width}px`, height: `${height}px`, backgroundColor: background } }>
              <div className="progressbar-box__fill" style={ { backgroundColor: fill, height: counterValue + `%` } }></div>
              <div className="progressbar-box__content">
                { showCounter && (
                  <p className="progressbar-box__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
                ) }
              </div>
            </div>
            <RichText
              tagName="p"
              placeholder={ __( 'Type title...', 'WLC' ) }
              className="progressbar-box__title"
              value={ title }
              onChange={ ( value ) => setAttributes( { title: value } ) }
              style={ { color: titleColor } }
            />
          </div>
        ) }
      </div>
    </div>
  );
}

export default edit;

index.js

/**
 * WordPress dependencies
 */
import { registerBlockType } from '@wordpress/blocks';

/**
 * Internal dependencies
 */
import './style.scss';
import edit from './edit';
import save from './save';
import metadata from './block.json';

registerBlockType( metadata.name, {
  edit,
  save,
} );

save.js

/**
 * WordPress dependencies
 */
import {
  useBlockProps,
  RichText,
} from '@wordpress/block-editor';

const save = ( { attributes } ) => {
  const { type, title, counterValue, showCounter, width, height, circleBorderWidth, titleColor, counterTextColor, background, fill, animate } = attributes;
  const blockProps = useBlockProps.save();

  const circleRightRotation = counterValue <= 50 ? ( counterValue / 50 ) * 180 : 180;
  const circleLeftRotation = counterValue > 50 ? 180 + ( ( counterValue - 50 ) / 50 ) * 180 : 0;
  const maxAngle = Math.max(circleLeftRotation,circleRightRotation);

  return (
     <div { ...blockProps }
      data-wp-interactive="wlc/progress-bar"
      data-wp-run="callbacks.animateBar"
      data-wp-context={ JSON.stringify({ animate, isAnimating: false }) }
    >
      { type == 'line' && (
        <div className="progressbar-line">
          <div className="progressbar-line__header">
            <RichText.Content
              tagName="p"
              className="progressbar-line__title"
              value={ title }
              style={ { color: titleColor } }
            />
            { showCounter && (
              <p className="progressbar-line__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
            ) }
          </div>
          <div className="progressbar-line__inner" style={ { height: `${height}px`, backgroundColor: background } }>
            <div className="progressbar-line__fill" style={ { backgroundColor: fill, '--width': `${counterValue}%` } }></div>
          </div>
        </div>
      ) }
      { ( type == 'circle' ) && (
        <div className="progressbar-circle" style={ { width: `${width}px`, height: `${width}px` } }>
          <div className="progressbar-circle__fill" style={ counterValue > 50 ? { clipPath: 'inset(0px)', '--angle': `${maxAngle}deg` } : {'--angle': `${maxAngle}deg`} }>
            <div
              className="progressbar-circle__fill-left"
              style={ { borderColor: fill, borderWidth: circleBorderWidth, transform: `rotate(${circleLeftRotation}deg)` } }
            ></div>
            <div
              className="progressbar-circle__fill-right"
              style={ { borderColor: fill, borderWidth: circleBorderWidth, transform: `rotate(${circleRightRotation}deg)` } }
            ></div>
          </div>
          <div className="progressbar-circle__shape" style={ { borderColor: background, borderWidth: circleBorderWidth } }></div>
          <div className="progressbar-circle__content">
            { title && (
              <RichText.Content
                tagName="p"
                className="progressbar-circle__title"
                value={ title }
                style={ { color: titleColor } }
              />
            ) }
            { showCounter && (
              <p className="progressbar-circle__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
            ) }
          </div>
        </div>
      ) }
       { ( type == 'circle-fill' ) && (
        <div className="progressbar-circle circle-fill" style={ { width: `${width}px`, height: `${width}px` } }>
          <div className="progressbar-circle__fill" style={ counterValue > 50 ? { clipPath: 'inset(0px)', '--angle': `${maxAngle}deg` } : {'--angle': `${maxAngle}deg`} }>
            <div
              className="progressbar-circle__fill-left"
              style={ { backgroundColor: fill, transform: `rotate(${circleLeftRotation}deg)` } }
            ></div>
            <div
              className="progressbar-circle__fill-right"
              style={ { backgroundColor: fill, transform: `rotate(${circleRightRotation}deg)` } }
            ></div>
          </div>
          <div className="progressbar-circle__shape" style={ { backgroundColor: background } }></div>
          <div className="progressbar-circle__content">
            { title && (
              <RichText.Content
                tagName="p"
                className="progressbar-circle__title"
                value={ title }
                style={ { color: titleColor } }
              />
            ) }
            { showCounter && (
              <p className="progressbar-circle__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
            ) }
          </div>
        </div>
      ) }
      { type == 'box' && (
        <div className="progressbar-box" style={ { width: `${width}px` } }>
          <div className="progressbar-box__inner" style={ { height: `${height}px`, backgroundColor: background } }>
            <div className="progressbar-box__fill" style={ { backgroundColor: fill, '--height': `${counterValue}%` } }></div>
            <div className="progressbar-box__content">
              { showCounter && (
                <p className="progressbar-box__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
              ) }
            </div>
          </div>
          <RichText.Content
            tagName="p"
            className="progressbar-box__title"
            value={ title }
            style={ { color: titleColor } }
          />
        </div>
      ) }
    </div>
  );
}

export default save;

style.scss

@property --angle {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}

.progressbar-line {
  &__header {
    margin-bottom: 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  &__title {
    font-size: 18px;
    margin: 0;
  }

  &__value {
    font-size: 18px;
    margin: 0;
  }

  &__inner {
    height: 15px;
    background: #f4f4f4;
    position: relative;
  }

  &__fill {
    top: 0;
    left: 0;
    bottom: 0;
    background: #000;
    position: absolute;
    width: var(--width);

    &.animate {
      width: 0;
    }
    &.animating {
      animation: 3s width both;
    }
  }

  @keyframes width {
    from {
      width: 0;
    }
    to {
      width: var(--width);
    }
  }
}

.progressbar-circle {
  width: 250px;
  height: 250px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;

  &.circle-fill {
    .progressbar-circle__fill-left,
    .progressbar-circle__fill-right,
    .progressbar-circle__shape {
      border-width: 0;
    }

    .progressbar-circle__value {
      font-size: 26px;
      font-weight: bold;
      margin: 0;
    }
  }

  &__fill {
    clip-path: inset(0 0 0 50%);
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    position: absolute;

    &-left,
    &-right {
      clip-path: inset(0 50% 0 0);
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      border: 15px solid #000;
      border-radius: 50%;
      box-sizing: border-box;
      position: absolute;
    }

    &-left {
      transform: rotate(0deg);
    }

    &-right {
      transform: rotate(180deg);
    }

    &.animate {
      mask-image: conic-gradient( #000 0deg, #000 0deg, transparent 0deg, transparent 360deg );
    }

    &.animating {
    mask-image: conic-gradient( #000 0deg, #000 var(--angle), transparent var(--angle), transparent 360deg );
      animation: 3s angle both;
    }

    @keyframes angle {
      from {
        --angle: 0deg;
      }

    }
  }

  &__shape {
    width: 100%;
    height: 100%;
    border: 15px solid #eee;
    border-radius: 50%;
    box-sizing: border-box;
  }

  &__content {
    text-align: center;
    position: absolute;
  }
}

.progressbar-box {
  &__inner {
    width: 100%;
    height: 310px;
    background: #f4f4f4;
    margin: 0 auto;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  &__fill {
    right: 0;
    left: 0;
    bottom: 0;
    background: #000;
    position: absolute;
    height: var(--height);

    &.animate {
      height: 0;
    }

    &.animating {
      animation: 3s height both;
    }

    @keyframes height{
      from {
        height: 0;
      }
      to {
        height: var(--height);
      }
    }

  }

  &__content {
    position: relative;
  }

  &__value {
    font-size: 26px;
    font-weight: bold;
    margin: 0;
  }

  &__title {
    text-align: center;
    margin-top: 15px;
  }
  
}

view.js

import { getElement, getContext, store, useState, useEffect } from '@wordpress/interactivity'

const useAnimation = () => {
    const [ animation, setAnimation ] = useState(false);

    useEffect( () => {
        const context = getContext();
        const { ref } = getElement();
        ref.querySelector('[class$="__fill"]').classList.toggle('animate', context.animate );
        setAnimation( context.animate );
    }, []);

    return animation;
}

const useInView = () => {
    const [ inView, setInView ] = useState(false);
    useEffect( () => {
        const { ref } = getElement();
        const observer = new IntersectionObserver( ( [ entry ] ) => {
               setInView( entry.isIntersecting );
        }, { threshold: 1 } );
        observer.observe( ref );
        return () => ref && observer.unobserve( ref );
    }, []);

    return inView;
}

const animate = () => {
    const context = getContext();
    context.isAnimating = true;
    const { ref } = getElement();
    ref.querySelector('[class ~= "animate"]').classList.add('animating');
}

store( 'wlc/progress-bar', {
    callbacks: {
        animateBar: () => {
            const withAnimation = useAnimation();
            const inView = useInView();
            useEffect( () => {
                const context = getContext();
                if ( withAnimation && inView && !context.isAnimating ) {
                    animate();
                }
            })
        }
    }
} )