Search code examples
javascripthtmlcssreactjscanvas

Writing to a canvas breaks the resizing of the wrapping div on Safari only


I have a div wrapping a canvas. I want to be able to resize the div. Im using resize: "both" and overflow: "auto" on the wrapping div. This works fine on Chrome, but bizarrely, on Safari, if I write anything to the canvas, I now CANNOT resize the div. The cursor also no longer changes when I hover over the resize animation in the bottom right.

The code is:

function Base() {

  React.useEffect(() => {
    const canvas = document.getElementById("covering-canvas-11");
    const context = canvas.getContext("2d");

    console.log("context for convercaning canvas 11 is ", context);

    if (context) {
      context.clearRect(0, 0, 40, 40);
      context.fillStyle = "white";
    }
  });

  return (
    <div>
        <div
          style={{
            resize: "both",
            overflow: "auto",
            background: "#ccc",
            width: "40px",
            height: "40px",
          }}
        >
          <canvas id={`covering-canvas-11`} width="40px" height="40px"></canvas>
        </div>
    </div>
  );
}
ReactDOM.render(
  <Base />,
  document.getElementById('root')
);

The CodePen is here: https://codepen.io/mark-kelly-the-looper/pen/gOjwjyz

This works on Chrome, I can hover over the div and expand it. But not on Safari. When I comment out the useEffect so there is no longer any writing to the canvas and check on Safari - now the div can be resized!

EDIT: CodePen with drawing to the canvas (div is NOT resizable on Safari): https://codepen.io/mark-kelly-the-looper/pen/rNrMELR

enter image description here

CodePen without drawing to canvas (div IS resizable in Safari): https://codepen.io/mark-kelly-the-looper/pen/rNrMELR

EDIT:

Im using Safari 14.0.3. If others cannot reproduce this issue, please comment.

EDIT My app allows users to draw shapes on the canvas, so canvas listens mouse move, mouse down, mouse up events.

enter image description here


Solution

  • It's a bug and it's reproducible.

    To fix it in Safari, add this to your canvas styles.

    position: relative;
    z-index: -1;
    

    And this to the container div.

    position: relative;
    z-index: 0;
    

    Modify the values for position and the z-index based on your real needs. But keep the combination and a negative value for z-index of the canvas, and 0 or more for the container div.

    So targeting Safari only, your code becomes:

    function Base() {
    
       React.useEffect(() => {
         const canvas = document.getElementById("covering-canvas-11");
         const context = canvas.getContext("2d");
    
         console.log("context for convercaning canvas 11 is ", context);
    
         if (context) {
           context.clearRect(0, 0, 40, 40);
           context.fillStyle = "9ff";
           context.fillRect(0, 0, 40, 40);
         }
       });
      
    function handleCanvasClick(e) {
      e.preventDefault();
      console.log('You clicked the canvas.');
    }
    
    const canvasSafariCSS = `
      /* CSS targeting Safari only */
      @media not all and (min-resolution:.001dpcm) {
        .canvas-container {
          position: relative;
          z-index: 0;
        }
        .canvas-container canvas {
          position: relative;
          z-index: -1;
        }
      }
    `;
    
      return (
        <div>
            <style>{canvasSafariCSS}</style>
            <div
              className = "canvas-container"
              style={{
                resize: "both",
                overflow: "auto",
                background: "#ccc",
                width: "40px",
                height: "40px",
              }}
            >
              <canvas id={`covering-canvas-11`} width="40px" height="40px"
                onClick = {handleCanvasClick}
              ></canvas>
            </div>
        </div>
      );
    }
    ReactDOM.render(
      <Base />,
      document.getElementById('root')
    );
    

    More explanation

    The gist of this method is to establish a stacking context (and this) for the container div and keep the position and a negative z-index for the canvas

    So in essence, for the container div, aside from

    position: relative;
    z-index: 0;
    

    Something like the following will work too:

    transform: translateX(0);
    

    or an opacity less than 1:

    opacity: 0.99;
    

    or even:

    will-change: opacity;
    

    e.g.

    function Base() {
    
       React.useEffect(() => {
         const canvas = document.getElementById("covering-canvas-11");
         const context = canvas.getContext("2d");
    
         console.log("context for convercaning canvas 11 is ", context);
    
         if (context) {
           context.clearRect(0, 0, 40, 40);
           context.fillStyle = "9ff";
           context.fillRect(0, 0, 40, 40);
         }
       });
      
    function handleCanvasClick(e) {
      e.preventDefault();
      console.log('You clicked the canvas.');
    }
    
    const canvasSafariCSS = `
      /* CSS targeting Safari only */
      @media not all and (min-resolution:.001dpcm) {
        .canvas-container {
          will-change: opacity;
        }
        .canvas-container canvas {
          position: relative;
          z-index: -1;
        }
      }
    `;
    
      return (
        <div>
            <style>{canvasSafariCSS}</style>
            <div
              className = "canvas-container"
              style={{
                resize: "both",
                overflow: "auto",
                background: "#ccc",
                width: "40px",
                height: "40px",
              }}
            >
              <canvas id={`covering-canvas-11`} width="40px" height="40px"
                onClick = {handleCanvasClick}
              ></canvas>
            </div>
        </div>
      );
    }
    ReactDOM.render(
      <Base />,
      document.getElementById('root')
    );