Modern Web Weekly #14
Staying up to date with the modern web
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:
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:
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 (window
, browser
, or monitor
) should be preselected. In Chrome, the option to share a browser tab is preselected when displaySurface
is not specified:
When displaySurface
is set to monitor
with:
navigator.mediaDevices.getDisplayMedia({
video: {displaySurface: 'monitor'}
});
the option to share the entire screen will be preselected:
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:
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:
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
:
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
});
Send me a DM on Twitter to let me know!