Merge pull request #1956 from umap-project/custom-prompt

chore: add custom prompt
This commit is contained in:
Yohan Boniface 2024-07-02 22:49:05 +02:00 committed by GitHub
commit 1839c48234
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 319 additions and 148 deletions

View file

@ -11,8 +11,29 @@
color: var(--text-color);
border-radius: 5px;
overflow-y: auto;
height: fit-content;
max-height: 90vh;
}
.umap-dialog .umap-close-link {
float: right;
width: 100px;
:where([data-component="no-dialog"]:not([hidden])) {
display: block;
position: relative;
}
:where([data-component*="dialog"] menu) {
display: flex;
gap: calc(var(--gutter) / 2);
justify-content: flex-end;
margin: 0;
padding: 0;
}
:where([data-component*="dialog"] [data-ref="fieldset"]) {
border: 0;
margin: unset;
padding: unset;
}
/* hack for Firefox */
@-moz-document url-prefix() {
[data-component="no-dialog"]:not([hidden]) {
inset-inline-start: 0;
transform: none;
}
}

View file

@ -1,4 +1,4 @@
[role="dialog"] {
.umap-alert[role="dialog"] {
box-sizing: border-box;
min-height: 46px;
line-height: 46px;
@ -20,36 +20,36 @@
width: max-content;
z-index: var(--zindex-alert);
}
[role="dialog"] > div {
.umap-alert[role="dialog"] > div {
margin: 0 auto;
min-width: 60%;
background-size: 20px;
background-position: 0 15px;
padding-left: 28px;
}
[role="dialog"][data-level="info"] > div {
.umap-alert[role="dialog"][data-level="info"] > div {
background-image: url('../../../img/alert-icon-info.svg');
background-repeat: no-repeat;
}
[role="dialog"][data-level="success"] > div {
.umap-alert[role="dialog"][data-level="success"] > div {
background-image: url('../../../img/alert-icon-success.svg');
background-repeat: no-repeat;
}
[role="dialog"][data-level="error"] > div {
.umap-alert[role="dialog"][data-level="error"] > div {
background-image: url('../../../img/alert-icon-error.svg');
background-repeat: no-repeat;
}
[role="dialog"][data-level="error"] {
.umap-alert[role="dialog"][data-level="error"] {
background-color: var(--color-darkRed);
}
[role="dialog"] a {
.umap-alert[role="dialog"] a {
text-decoration: underline;
}
[role="dialog"] label {
.umap-alert[role="dialog"] label {
font-size: .8rem;
font-weight: normal;
}
[role="dialog"] a[target="_blank"] {
.umap-alert[role="dialog"] a[target="_blank"] {
background: url('../../../img/icon-external-link.svg') no-repeat right center;
padding-right: 14px;
background-size: 12px;
@ -127,7 +127,7 @@ h3[role="alert"] + p {
#link-wrapper {
margin-bottom: 1rem;
}
[role="dialog"] #conflict-wrapper a[target="_blank"] {
.umap-alert[role="dialog"] #conflict-wrapper a[target="_blank"] {
background-position-y: 16px;
}

View file

@ -45,6 +45,7 @@ export class BaseAutocomplete {
placeholder: this.options.placeholder,
autocomplete: 'off',
className: this.options.className,
name: this.options.name || 'autocomplete'
})
DomEvent.on(this.input, 'keydown', this.onKeyDown, this)
DomEvent.on(this.input, 'keyup', this.onKeyUp, this)

View file

@ -165,6 +165,7 @@ const ENTRIES = {
export default class Help {
constructor(map) {
this.map = map
this.dialog = new U.Dialog()
this.isMacOS = /mac/i.test(
// eslint-disable-next-line compat/compat -- Fallback available.
navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform
@ -207,7 +208,7 @@ export default class Help {
})
}
}
this.map.dialog.open({ content: container, className: 'dark' })
this.dialog.open({ template: container, className: 'dark', cancel: false, accept: false })
}
button(container, entries, classname) {
@ -241,10 +242,10 @@ export default class Help {
const actionsContainer = DomUtil.create('ul', 'umap-edit-actions', container)
const addAction = (action) => {
const actionContainer = DomUtil.add('li', '', actionsContainer)
DomUtil.add('i', action.options.className, actionContainer),
DomUtil.add('span', '', actionContainer, action.options.tooltip)
DomUtil.add('i', action.options.className, actionContainer)
DomUtil.add('span', '', actionContainer, action.options.tooltip)
DomEvent.on(actionContainer, 'click', action.addHooks, action)
DomEvent.on(actionContainer, 'click', this.map.dialog.close, this.map.dialog)
DomEvent.on(actionContainer, 'click', this.dialog.close, this.dialog)
}
title.textContent = translate('Where do we go from here?')
for (const id in this.map.helpMenuActions) {

View file

@ -53,7 +53,7 @@ export default class Importer {
this.TYPES = ['geojson', 'csv', 'gpx', 'kml', 'osm', 'georss', 'umap']
this.IMPORTERS = []
this.loadImporters()
this.dialog = new Dialog(this.map._controlContainer)
this.dialog = new Dialog()
}
loadImporters() {
@ -114,7 +114,7 @@ export default class Importer {
}
get action() {
return this.qs('[name=action]:checked').value
return this.qs('[name=action]:checked')?.value
}
get layerId() {
@ -234,7 +234,7 @@ export default class Importer {
}
submit() {
let hasErrors = false
let hasErrors
if (this.format === 'umap') {
hasErrors = !this.full()
} else if (!this.url) {
@ -242,7 +242,7 @@ export default class Importer {
} else if (this.action) {
hasErrors = !this[this.action]()
}
if (!hasErrors) {
if (hasErrors === false) {
Alert.info(translate('Data successfully imported!'))
}
}

View file

@ -37,8 +37,10 @@ export class Importer {
this.autocomplete = new Autocomplete(container, options)
importer.dialog.open({
content: container,
template: container,
className: `${this.id} importer dark`,
cancel: false,
accept: false,
})
}
}

View file

@ -30,13 +30,15 @@ export class Importer {
importer.format = select.options[select.selectedIndex].dataset.format
importer.layerName = select.options[select.selectedIndex].textContent
}
importer.dialog.close()
}
L.DomUtil.createButton('', container, translate('Choose this dataset'), confirm)
importer.dialog.open({
content: container,
className: `${this.id} importer dark`,
})
importer.dialog
.open({
template: container,
className: `${this.id} importer dark`,
accept: translate('Choose this dataset'),
cancel: false,
})
.then(confirm)
}
}

View file

@ -25,7 +25,6 @@ const TEMPLATE = `
</label>
<label id="boundary">
</label>
<button class="button">${translate('Choose this data')}</button>
`
class Autocomplete extends SingleMixin(BaseAjax) {
@ -66,7 +65,6 @@ export class Importer {
} else {
console.error(response)
}
const asPoint = container.querySelector('[name=aspoint]')
this.autocomplete = new Autocomplete(container.querySelector('#boundary'), {
placeholder: translate('Search admin boundary'),
url: `${this.baseUrl}/boundaries/search?text={q}`,
@ -75,21 +73,23 @@ export class Importer {
boundaryName = choice.item.label
},
})
const confirm = () => {
const confirm = (form) => {
if (!boundary || !select.value) {
Alert.error(translate('Please choose a theme and a boundary first.'))
return
}
importer.url = `${this.baseUrl}/data/${select.value}/${boundary}?format=geojson&aspoint=${asPoint.checked}`
importer.url = `${this.baseUrl}/data/${form.theme}/${boundary}?format=geojson&aspoint=${Boolean(form.aspoint)}`
importer.format = 'geojson'
importer.layerName = `${boundaryName}${select.options[select.selectedIndex].textContent}`
importer.dialog.close()
}
DomEvent.on(container.querySelector('button'), 'click', confirm)
importer.dialog.open({
content: container,
className: `${this.id} importer dark`,
})
importer.dialog
.open({
template: container,
className: `${this.id} importer dark`,
accept: translate('Choose this data'),
cancel: false,
})
.then(confirm)
}
}

View file

@ -11,7 +11,7 @@ const TEMPLATE = `
</label>
<label>
${translate('Geometry mode')}
<select name="out-mode">
<select name="out">
<option value="geom" selected>${translate('Default')}</option>
<option value="center">${translate('Only geometry centers')}</option>
</select>
@ -58,27 +58,28 @@ export class Importer {
})
this.map.help.parse(container)
const confirm = () => {
let tags = container.querySelector('[name=tags]').value
if (!tags) {
Alert.error(translate('Please define an expression for the query first'))
const confirm = (form) => {
if (!form.tags) {
Alert.error(translate('Expression is empty'))
return
}
const outMode = container.querySelector('[name=out-mode]').value
let tags = form.tags
if (!tags.startsWith('[')) tags = `[${tags}]`
let area = '{south},{west},{north},{east}'
if (boundary) area = `area:${boundary}`
const query = `[out:json];nwr${tags}(${area});out ${outMode};`
const query = `[out:json];nwr${tags}(${area});out ${form.out};`
importer.url = `${this.baseUrl}?data=${query}`
if (boundary) importer.layerName = boundaryName
importer.format = 'osm'
importer.dialog.close()
}
L.DomUtil.createButton('', container, translate('Choose this data'), confirm)
importer.dialog.open({
content: container,
className: `${this.id} importer dark`,
})
importer.dialog
.open({
template: container,
className: `${this.id} importer dark`,
accept: translate('Choose this data'),
cancel: false
})
.then(confirm)
}
}

View file

@ -1,23 +1,96 @@
import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
// From https://css-tricks.com/replace-javascript-dialogs-html-dialog-element/
export default class Dialog {
constructor(parent) {
this.parent = parent
this.className = 'umap-dialog window'
this.container = DomUtil.create('dialog', this.className, this.parent)
DomEvent.disableClickPropagation(this.container)
DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu.
DomEvent.on(this.container, 'wheel', DomEvent.stopPropagation)
DomEvent.on(this.container, 'MozMousePixelScroll', DomEvent.stopPropagation)
constructor(settings = {}) {
this.settings = Object.assign(
{
accept: translate('OK'),
cancel: translate('Cancel'),
className: '',
message: '',
template: '',
},
settings
)
this.init()
}
get visible() {
return this.container.open
collectFormData(formData) {
const object = {}
formData.forEach((value, key) => {
if (!Reflect.has(object, key)) {
object[key] = value
return
}
if (!Array.isArray(object[key])) {
object[key] = [object[key]]
}
object[key].push(value)
})
return object
}
close() {
this.container.close()
getFocusable() {
return [
...this.dialog.querySelectorAll(
'button,[href],select,textarea,input:not([type="hidden"]),[tabindex]:not([tabindex="-1"])'
),
]
}
init() {
this.dialogSupported = typeof HTMLDialogElement === 'function'
this.dialog = document.createElement('dialog')
this.dialog.role = 'dialog'
this.dialog.dataset.component = this.dialogSupported ? 'dialog' : 'no-dialog'
this.dialog.innerHTML = `
<form method="dialog" data-ref="form">
<ul class="buttons">
<li><i class="icon icon-16 icon-close" data-close></i></li>
</ul>
<h3 data-ref="message" id="${Math.round(Date.now()).toString(36)}"></h3>
<fieldset data-ref="fieldset" role="document">
<div data-ref="template"></div>
</fieldset>
<menu>
<button type="button" data-ref="cancel" data-close value="cancel"></button>
<button type="submit" class="button" data-ref="accept" value="accept"></button>
</menu>
</form>`
document.body.appendChild(this.dialog)
this.elements = {}
this.focusable = []
this.dialog
.querySelectorAll('[data-ref]')
.forEach((el) => (this.elements[el.dataset.ref] = el))
this.dialog.setAttribute('aria-labelledby', this.elements.message.id)
this.dialog.addEventListener('click', (event) => {
if (event.target.closest('[data-close]')) {
this.close()
}
})
if (!this.dialogSupported) {
this.elements.form.addEventListener('submit', (event) => {
event.preventDefault()
this.dialog.returnValue = 'accept'
this.close()
this.dialog.returnValue = undefined
})
}
this.dialog.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
if (!this.dialogSupported) {
e.preventDefault()
this.elements.form.requestSubmit()
}
}
if (e.key === 'Escape') {
e.stopPropagation()
this.close()
}
})
}
currentZIndex() {
@ -28,25 +101,88 @@ export default class Dialog {
)
}
open({ className, content, modal } = {}) {
this.container.innerHTML = ''
const currentZIndex = this.currentZIndex()
if (currentZIndex) this.container.style.zIndex = currentZIndex + 1
if (modal) this.container.showModal()
else this.container.show()
if (className) {
// Reset
this.container.className = this.className
this.container.classList.add(...className.split(' '))
open(settings = {}) {
const dialog = Object.assign({}, this.settings, settings)
this.dialog.className = 'umap-dialog window'
if (dialog.className) {
this.dialog.classList.add(...dialog.className.split(' '))
}
const buttonsContainer = DomUtil.create('ul', 'buttons', this.container)
const closeButton = DomUtil.createButtonIcon(
DomUtil.create('li', '', buttonsContainer),
'icon-close',
translate('Close')
this.elements.accept.textContent = dialog.accept
this.elements.accept.hidden = !dialog.accept
this.elements.cancel.textContent = dialog.cancel
this.elements.cancel.hidden = !dialog.cancel
this.elements.message.textContent = dialog.message
this.elements.message.hidden = !dialog.message
this.elements.target = dialog.target || ''
this.elements.template.innerHTML = ''
if (dialog.template?.nodeType === 1) {
this.elements.template.appendChild(dialog.template)
} else {
this.elements.template.innerHTML = dialog.template || ''
}
this.focusable = this.getFocusable()
this.hasFormData = this.elements.fieldset.elements.length > 0
const currentZIndex = this.currentZIndex()
if (currentZIndex) this.dialog.style.zIndex = currentZIndex + 1
this.toggle(true)
if (this.hasFormData) this.focusable[0].focus()
else this.elements.accept.focus()
return this.waitForUser()
}
close() {
this.toggle(false)
}
toggle(open = false) {
if (this.dialogSupported) {
if (open) this.dialog.show()
else this.dialog.close()
} else {
this.dialog.hidden = !open
if (this.elements.target && !open) {
this.elements.target.focus()
}
if (!open) {
this.dialog.dispatchEvent(new CustomEvent('close'))
}
}
}
waitForUser() {
return new Promise((resolve) => {
this.dialog.addEventListener(
'close',
(event) => {
if (this.dialog.returnValue === 'accept') {
const value = this.hasFormData
? this.collectFormData(new FormData(this.elements.form))
: true
resolve(value)
}
},
{ once: true }
)
})
}
alert(config) {
return this.open(
Object.assign({}, config, { cancel: false, message, template: false })
)
DomEvent.on(closeButton, 'click', this.close, this)
this.container.appendChild(buttonsContainer)
this.container.appendChild(content)
}
confirm(message, config = {}) {
return this.open(Object.assign({}, config, { message, template: false }))
}
prompt(message, fallback = '', config = {}) {
const template = `<input type="text" name="prompt" value="${fallback}">`
return this.open(Object.assign({}, config, { message, template }))
}
}

View file

@ -194,16 +194,9 @@ U.FeatureMixin = {
},
getAdvancedEditActions: function (container) {
L.DomUtil.createButton(
'button umap-delete',
container,
L._('Delete'),
function (e) {
L.DomEvent.stop(e)
if (this.confirmDelete()) this.map.editPanel.close()
},
this
)
L.DomUtil.createButton('button umap-delete', container, L._('Delete'), (e) => {
this.confirmDelete().then(() => this.map.editPanel.close())
})
},
appendEditFieldsets: function (container) {
@ -272,8 +265,11 @@ U.FeatureMixin = {
this.bindPopup(new Class(this))
},
confirmDelete: function () {
if (confirm(L._('Are you sure you want to delete the feature?'))) {
confirmDelete: async function () {
const confirmed = await this.map.dialog.confirm(
L._('Are you sure you want to delete the feature?')
)
if (confirmed) {
this.del()
return true
}

View file

@ -56,8 +56,8 @@ U.Map = L.Map.extend({
this.urls = new U.URLs(this.options.urls)
this.panel = new U.Panel(this)
this.dialog = new U.Dialog({ className: 'dark' })
this.tooltip = new U.Tooltip(this._controlContainer)
this.dialog = new U.Dialog(this._controlContainer)
if (this.hasEditMode()) {
this.editPanel = new U.EditPanel(this)
this.fullPanel = new U.FullPanel(this)
@ -539,18 +539,16 @@ U.Map = L.Map.extend({
initShortcuts: function () {
const globalShortcuts = function (e) {
if (e.key === 'Escape') {
if (this.dialog.visible) {
this.dialog.close()
} else if (this.importer.dialog.visible) {
if (this.importer.dialog.visible) {
this.importer.dialog.close()
} else if (this.editEnabled && this.editTools.drawing()) {
this.editTools.stopDrawing()
} else if (this.measureTools.enabled()) {
this.measureTools.stopDrawing()
} else if (this.editPanel?.isOpen()) {
this.editPanel?.close()
} else if (this.fullPanel?.isOpen()) {
this.fullPanel?.close()
} else if (this.editPanel?.isOpen()) {
this.editPanel?.close()
} else if (this.panel.isOpen()) {
this.panel.close()
}
@ -1642,9 +1640,12 @@ U.Map = L.Map.extend({
},
askForReset: function (e) {
if (!confirm(L._('Are you sure you want to cancel your changes?'))) return
this.reset()
this.disableEdit()
this.dialog
.confirm(L._('Are you sure you want to cancel your changes?'))
.then(() => {
this.reset()
this.disableEdit()
})
},
startMarker: function () {

View file

@ -21,35 +21,8 @@ U.TableEditor = L.Class.extend({
const rename = L.DomUtil.create('i', 'umap-edit', container)
del.title = L._('Delete this property on all the features')
rename.title = L._('Rename this property on all the features')
const doDelete = function () {
if (
confirm(
L._('Are you sure you want to delete this property on all the features?')
)
) {
this.datalayer.eachLayer((feature) => {
feature.deleteProperty(property)
})
this.datalayer.deindexProperty(property)
this.resetProperties()
this.edit()
}
}
const doRename = function () {
const newName = prompt(
L._('Please enter the new name of this property'),
property
)
if (!newName || !this.validateName(newName)) return
this.datalayer.eachLayer((feature) => {
feature.renameProperty(property, newName)
})
this.datalayer.deindexProperty(property)
this.datalayer.indexProperty(newName)
this.edit()
}
L.DomEvent.on(del, 'click', doDelete, this)
L.DomEvent.on(rename, 'click', doRename, this)
L.DomEvent.on(del, 'click', () => this.deleteProperty(property))
L.DomEvent.on(rename, 'click', () => this.renameProperty(property))
},
renderRow: function (feature) {
@ -89,6 +62,45 @@ U.TableEditor = L.Class.extend({
return true
},
renameProperty: function (property) {
this.datalayer.map.dialog
.prompt(L._('Please enter the new name of this property'))
.then(({ prompt }) => {
if (!prompt || !this.validateName(prompt)) return
this.datalayer.eachLayer((feature) => {
feature.renameProperty(property, prompt)
})
this.datalayer.deindexProperty(property)
this.datalayer.indexProperty(prompt)
this.edit()
})
},
deleteProperty: function (property) {
this.datalayer.map.dialog
.confirm(
L._('Are you sure you want to delete this property on all the features?')
)
.then(() => {
this.datalayer.eachLayer((feature) => {
feature.deleteProperty(property)
})
this.datalayer.deindexProperty(property)
this.resetProperties()
this.edit()
})
},
addProperty: function () {
this.datalayer.map.dialog
.prompt(L._('Please enter the name of the property'))
.then(({ prompt }) => {
if (!prompt || !this.validateName(prompt)) return
this.datalayer.indexProperty(prompt)
this.edit()
})
},
edit: function () {
const id = 'tableeditor:edit'
this.compileProperties()
@ -102,13 +114,7 @@ U.TableEditor = L.Class.extend({
)
const iconElement = L.DomUtil.createIcon(addButton, 'icon-add')
addButton.insertBefore(iconElement, addButton.firstChild)
const addProperty = function () {
const newName = prompt(L._('Please enter the name of the property'))
if (!newName || !this.validateName(newName)) return
this.datalayer.indexProperty(newName)
this.edit()
}
L.DomEvent.on(addButton, 'click', addProperty, this)
L.DomEvent.on(addButton, 'click', this.addProperty, this)
this.datalayer.map.fullPanel.open({
content: this.table,
className: 'umap-table-editor',

View file

@ -5,7 +5,7 @@
</style>
<template id="umap-alert-template">
<div role="dialog" class="dark window">
<div role="dialog" class="dark window umap-alert">
<div>
<p role="alert"></p>
</div>
@ -20,7 +20,7 @@
<umap-alert></umap-alert>
<template id="umap-alert-creation-template">
<div role="dialog" class="dark window">
<div role="dialog" class="dark window umap-alert">
<div>
<h3 role="alert"></h3>
{% url "login" as login_url %}
@ -55,7 +55,7 @@
<umap-alert-creation></umap-alert-creation>
<template id="umap-alert-conflict-template">
<div role="dialog" class="dark window">
<div role="dialog" class="dark window umap-alert">
<div>
<p role="alert"></p>
<div id="conflict-wrapper">

View file

@ -320,10 +320,11 @@ def test_should_redraw_list_on_feature_delete(live_server, openmap, page, bootst
page.get_by_role("button", name="Edit").click()
buttons = page.locator(".umap-browser .datalayer li .icon-delete")
expect(buttons).to_have_count(3)
page.on("dialog", lambda dialog: dialog.accept())
buttons.nth(0).click()
buttons.first.click()
page.locator("dialog").get_by_role("button", name="OK").click()
expect(buttons).to_have_count(2)
page.get_by_role("button", name="Cancel edits").click()
page.locator("dialog").get_by_role("button", name="OK").click()
expect(buttons).to_have_count(3)

View file

@ -67,6 +67,7 @@ def test_cancel_deleting_datalayer_should_restore(
expect(page.get_by_text("test datalayer")).to_be_hidden()
page.once("dialog", lambda dialog: dialog.accept())
page.get_by_role("button", name="Cancel edits").click()
page.locator("dialog").get_by_role("button", name="OK").click()
expect(markers).to_have_count(1)
expect(page.locator(".umap-browser").get_by_text("test datalayer")).to_be_visible()

View file

@ -117,8 +117,8 @@ def test_should_reset_style_on_cancel(live_server, openmap, page, bootstrap):
expect(page.locator(".leaflet-overlay-pane path[fill='GoldenRod']")).to_have_count(
1
)
page.once("dialog", lambda dialog: dialog.accept())
page.get_by_role("button", name="Cancel edits").click()
page.locator("dialog").get_by_role("button", name="OK").click()
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)

View file

@ -9,12 +9,14 @@ def test_table_editor(live_server, openmap, datalayer, page):
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
page.get_by_role("link", name="Manage layers").click()
page.locator(".panel").get_by_title("Edit properties in a table").click()
page.once("dialog", lambda dialog: dialog.accept(prompt_text="newprop"))
page.get_by_text("Add a new property").click()
page.locator("dialog").locator("input").fill("newprop")
page.locator("dialog").get_by_role("button", name="OK").click()
page.locator('input[name="newprop"]').fill("newvalue")
page.once("dialog", lambda dialog: dialog.accept())
page.hover(".umap-table-editor .tcell")
page.get_by_title("Delete this property on all").first.click()
page.locator("dialog").get_by_role("button", name="OK").click()
with page.expect_response(re.compile(r".*/datalayer/update/.*")):
page.get_by_role("button", name="Save").click()
saved = DataLayer.objects.last()

View file

@ -69,8 +69,8 @@ def test_websocket_connection_can_sync_markers(
# Delete a marker from peer A and check it's been deleted on peer B
a_first_marker.click(button="right")
peerA.on("dialog", lambda dialog: dialog.accept())
peerA.get_by_role("link", name="Delete this feature").click()
peerA.locator("dialog").get_by_role("button", name="OK").click()
expect(a_marker_pane).to_have_count(1)
expect(b_marker_pane).to_have_count(1)
@ -153,8 +153,8 @@ def test_websocket_connection_can_sync_polygons(
# Delete a polygon from peer A and check it's been deleted on peer B
a_polygon.click(button="right")
peerA.on("dialog", lambda dialog: dialog.accept())
peerA.get_by_role("link", name="Delete this feature").click()
peerA.locator("dialog").get_by_role("button", name="OK").click()
expect(a_polygons).to_have_count(0)
expect(b_polygons).to_have_count(0)