Modern Web Weekly #14

Modern Web Weekly #14

Staying up to date with the modern web

👋
Hello there! I'm Danny and this is Modern Web Weekly, your weekly update on what the modern web is capable of, Web Components, and Progressive Web Apps (PWA). I test modern web features and write about them in plain English to make sure you stay up to date.

Shortcuts for PWAs on MacOS 🎉

Since Apple doesn't list everything in their release notes I don't know when this was implemented, but I just found out that shortcuts for PWAs now work on MacOS.

Shortcuts are quick actions that can be invoked through a context menu that appears when you right-click the PWA icon in the Dock on MacOS:

PWA shortcuts on MacOS

Strangely enough, these shortcuts are only shown when the PWA is already running. Let's hope that's a bug that will be fixed soon.

Shortcuts have already been available on Windows and ChromeOS and also on Android where they can be accessed when you press and hold the PWA icon:

PWA shortcuts on Android

You can add shortcuts to your PWA by adding them to the shortcuts section of manifest.json:

"shortcuts": [
    {
      "name": "Media Capture",
      "short_name": "Media Capture",
      "description": "Media Capture allows apps to use the camera and    
                      microphone of a device",
      "url": "/media",
      "icons": [
        {
          "src": "/src/img/icons/mediacapture-96x96.png",
          "sizes": "96x96"
        }
      ]
    }
]

Each shortcut has a url field that points to the page in the PWA that will be opened when the shortcut is activated. The name field is the name that is shown in the context menu. When there isn't enough space short_name will be displayed instead. The icons field is an array of icons that the browser can choose from to show next to the name of the shortcut. The icon should be at least 96x96px, but this is not (yet) implemented on MacOS.

Shortcuts on MacOS are currently only available for PWAs installed through Chrome and Edge, not (yet) for web apps added to the Dock through Safari. Hopefully, this is another step towards full support of PWAs on MacOS!

Invokers are coming to a browser near you

Invokers enable developers to assign behavior to buttons in a more declarative and accessible way. In plain English: less JavaScript and less bugs!

The basic idea is that you add an invoketarget attribute to a <button> that holds the id of the element that is the target of the invoked action:

<button invoketarget="my-popover">Open Popover</button>
<div id="my-popover" popover="auto">This is a popover</div>

In this example, when the button is clicked it will open the popover. This is the default behavior of the invoker (the button) which is equivalent to setting the invokeaction attribute to auto.

Invokers are currently implemented in Chrome Canary with Experimental Web Features enabled and in Firefox Nightly with dom.element.invokers.enabled in about:config. Default behavior is implemented in Chrome Canary when the target is a popover, as shown in the previous example, and when the target is a <details> element in both Chrome Canary and Firefox Nightly:

<button invoketarget="my-details">Open Details</button>
  <details id="my-details">
    <summary>Summary...</summary>
    Hello world!
  </details>

You can also specify a predefined behavior in invokeaction, for example showPicker to open the file picker that is displayed when you click a file input:

<button invoketarget="my-file" invokeaction="showPicker">
  Pick a file
</button>
<input id="my-file" type="file">

This will open the file picker in Chrome Canary.

Custom behavior with invokers

Where invokers really shine is when you implement custom behavior. When a button with invoketarget is clicked it dispatches an InvokeEvent on the target that has an action property that holds the value of the invokeaction attribute of the button. This way, you can implement two buttons that show and hide a <dialog> respectively:

<button invoketarget="my-dialog" invokeaction="show">
  Open dialog with custom behavior
</button>
<button invoketarget="my-dialog" invokeaction="close">
  Close dialog with custom behavior
</button>
<dialog id="my-dialog">This is the dialog</dialog>

<script>
const dialog = document.querySelector('#my-dialog');

dialog.addEventListener('invoke', ({action}) => {
  if(action === 'show') {
    dialog.show();
  }
  if(action === 'close') {
    dialog.close();
  }
 });
</script>

The beauty of this is that the event is invoked on the target instead of the buttons. This way, you can make as many buttons as you want invoke an action with a single event handler. A simple but brilliant paradigm shift.

Invokers is currently a proposal from Open UI, a W3C Community group whose purpose is to allow web developers to style and extend built-in web UI components and controls and was written by Keith Cirkel (@Keithamus)

So right now it's a proposal that's only implemented by Chrome Canary and Firefox Nightly but their popover proposal has already been implemented in all browsers except Firefox (that will implement it soon as well).

Check out the Invokers explainer that lists all the details and Keith's counter demo.

A real Chrome on iOS?

This week, Bloomberg reported that iPhone users in the EU will next year be able to download apps hosted outside of Apple's official App Store to comply with European regulations. The European Union's Digital Markets Act (DMA), which went into effect on November 1, 2022 requires so-called "gatekeeper" companies to open their services and platforms to other companies and developers.

These companies don't have to comply with these rules until 2024 which means this will land in iOS 17 which will be released in the first half of next year.

According to Bloomberg, Apple is considering removing its requirement for other browsers to use Webkit and opening up its APIs so third-party can interact with Apple’s hardware and core system functions.

If all this is true, this would mean that a real Chrome on iOS that has access to the same OS features it has on Android (vibration, file system, Bluetooth, sensors, etc) could soon become a reality.

This would give an enormous boost to PWAs as they would now have (roughly) the same capabilities on Android and iOS, at least for PWAs installed through Chrome.

Stay tuned for more! 🎉

Better controls for screen sharing

Recently, extra options have been added to the configuration object for getDisplayMedia to improve the amount of control developers have over screen sharing to improve the user experience.

The Screen Capture API enables web apps to capture (part of) the screen, windows or browser tabs for streaming, recording or sharing. Capturing is initiated with a call to navigator.mediaDevices.getDisplayMedia() which returns a MediaStream containing the shared content as audio and video tracks.

This method takes an optional configuration object that specifies requirements for the returned MediaStream. The video property can take true to indicate that the stream includes video (default) or it can take a MediaTrackConstraints object that further configures the video stream like aspect ratio, dimension and frame rate among others.

In all browsers except Firefox, the object can take a displaySurface property that specifies which type of display surface (windowbrowser, or monitor) should be preselected. In Chrome, the option to share a browser tab is preselected when displaySurface is not specified:

Screen capture in Chrome with Chrome Tab preselected (default)

When displaySurface is set to monitor with:

navigator.mediaDevices.getDisplayMedia({
  video: {displaySurface: 'monitor'}
});

the option to share the entire screen will be preselected:

Screen capture in Chrome with displaySurface: 'monitor'

In addition to audio and video the configuration object can take the surfaceSwitching property which is currently supported by Chrome and Edge. When sharing a browser tab, this property specifies whether or not the user can pick another tab for sharing while capturing is taking place. The default value is exclude but when it's set to include, a banner will appear inside each browser tab that is currently not captured which contains a button that the user can click to share that tab instead of the current one:

Screen capture in Chrome with surfaceSwitching: 'include'

Preserving privacy while sharing

When sharing your screen, privacy is important and the configuration object has several options to ensure it's preserved. In some cases, it may not be desirable to share your entire screen and this can be configured with the monitorTypeSurfaces property. Its default value is include but when set to exclude, the option to share the entire screen is removed:

Screen capture in Chrome with monitorTypeSurfaces: 'exclude'

If you are capturing from a browser tab you may not want to offer that tab for sharing for privacy reasons or to prevent the "hall of mirrors" effect, where the capturing tab is capturing itself which shows and endless "hall" of screens inside each other.

When the selfBrowserSurface property is set to exclude, the tab that is currently capturing the screen will be removed from the list of browser tabs that can be captured. The default value is include.

But there may also be cases where the current tab is the only one that should be shared. To make only the current tab available for sharing, set preferCurrentTab to true:

Screen capture in Chrome with preferCurrentTab: true

Keeping the capturing tab focused

When screen capturing is started, the focus will shift from the capturing tab to the content (screen, window, or tab) that is captured. This may not always be what you want and can even confuse the user.

Chrome and Edge now support the CaptureController object that enables web apps to keep the focus on the capturing tab by passing the CaptureController in the controller property of the configuration object.

CaptureController has a single method setFocusBehavior which can take the values focus-captured-surface (default) and no-focus-change which keeps the capturing tab focused.

Here's a full example:

if ('CaptureController' in window && 
    'setFocusBehavior' in CaptureController.prototype) {
      controller = new CaptureController();
      controller.setFocusBehavior('no-focus-change');
    }

this.screenStream = await navigator.mediaDevices.getDisplayMedia({
  video: {
    displaySurface: 'monitor', // monitor, window, browser
  },
  audio: true,
  surfaceSwitching: 'include',      // include, exclude
  selfBrowserSurface: 'exclude',    // include, exclude
  preferCurrentTab: false,          // true, false
  monitorTypeSurfaces: "include",   // include, exclude
  ...(controller && {controller})   // only add controller when supported
});
🔗
Got an interesting link for Modern Web Weekly?
Send me a DM on Twitter to let me know!