用新的HTML对话框元素替换JavaScript对话框

目录

一个对话框类

替换JavaScript对话框的初始模板

检查支持

DOM节点引用

按钮属性

“取消”按钮

填充不支持的浏览器

键盘导航

显示

隐藏

去哪里:focus?

添加警报、确认和提示

alert()通常是这样触发的:

confirm()通常是这样触发的:

prompt()通常是这样触发的:

我们应该测试一下

异步/等待

跨浏览器样式

自定义对话框示例

现场演示


您知道JavaScript对话框是如何用于警告、确认和提示用户操作的吗?假设您想用新的HTML对话框元素替换JavaScript对话框。

让我解释。

我最近参与了一个项目,其中有很多API调用和通过JavaScript对话框收集的用户反馈。在等待其他开发人员编写<Modal />组件代码时,我使用了alert(),confirm()prompt()在我的代码中。例如:

const deleteLocation = confirm('Delete location');
if (deleteLocation) {
  alert('Location deleted');
}

然后我突然想到:你可以通过alert() confirm()prompt()免费获得很多与模态相关的特性,而这些特性经常被忽略:

  • 这是一个真正的模态。例如,它总是在堆栈的顶部——即在<div>为z-index: 99999;之上。
  • 它可以通过键盘访问。按Enter接受和Escape取消。
  • 它对屏幕阅读器很友好。它移动焦点并允许大声朗读模态内容。
  • 它会吸引注意力。按下Tab不会到达主页上的任何可聚焦元素,但在Firefox和Safari中,它确实会将焦点移到浏览器UI。奇怪的是,您无法使用该Tab键将焦点移至任何浏览器中的“接受”或“取消”按钮。
  • 它支持用户偏好。我们开箱即可获得自动明暗模式支持。
  • 它暂停代码执行。另外,它等待用户输入。

当我需要这些功能中的任何一个时,这三种JavaScript方法99%的时间都可以工作。那么为什么我——或者真的是任何其他网络开发者——不使用它们呢?可能是因为它们看起来像无法设置样式的系统错误。另一个重要的考虑因素是:它们已经被弃用了。首先从跨域iframe中删除,并且,换句话说,完全从Web平台中删除,尽管听起来这方面的计划也被搁置了。

考虑到这一点,我们必须用哪些alert()confirm()prompt()替代它们呢?您可能已经听说过<dialog>HTML 元素,这就是我想在本文中看到的内容,将它与JavaScript一起使用class

用相同的功能完全替换Javascript对话框是不可能的,但是如果我们使用与可以(接受)或(取消)相结合的showModal()方法,那么我们就有了几乎一样好的东西。哎呀,当我们这样做的时候,让我们为HTML对话框元素添加声音——就像真正的系统对话框一样!

如果您想立即观看演示,请点击此处

一个对话框类

首先,我们需要一个基本的JavaScript Class,它带有一个设置对象,将与默认设置合并。这些设置将用于所有对话框,除非您在调用它们时覆盖它们(但稍后会详细介绍)。

export default class Dialog {
constructor(settings = {}) {
  this.settings = Object.assign(
    {
      /* DEFAULT SETTINGS - see description below */
    },
    settings
  )
  this.init()
}

设置如下:

  • accept这是“接受”按钮的标签。
  • bodyClass这是一个CSS类,当浏览器支持和不支持<dialog>对话框打开时添加到元素<body>中。
  • cancel这是“取消”按钮的标签。
  • dialogClass这是添加到<dialog>元素的自定义CSS类。
  • message:这是<dialog>里面的内容。
  • soundAccept这是当用户点击“接受”按钮时我们将播放的声音文件的URL。
  • soundOpen这是用户打开对话框时我们将播放的声音文件的URL。
  • template这是一个可选的小HTML模板,它被注入到<dialog>中。

替换JavaScript对话框的初始模板

在该init方法中,我们将添加一个帮助函数来检测浏览器对HTML对话框元素的支持,并设置基本HTML

init() {
  // Testing for <dialog> support
  this.dialogSupported = typeof HTMLDialogElement === 'function'
  this.dialog = document.createElement('dialog')
  this.dialog.dataset.component = this.dialogSupported ? 'dialog' : 'no-dialog'
  this.dialog.role = 'dialog'
  
  // HTML template
  this.dialog.innerHTML = `
  <form method="dialog" data-ref="form">
    <fieldset data-ref="fieldset" role="document">
      <legend data-ref="message" id="${(Math.round(Date.now())).toString(36)}">
      </legend>
      <div data-ref="template"></div>
    </fieldset>
    <menu>
      <button data-ref="cancel" value="cancel"></button>
      <button data-ref="accept" value="default"></button>
    </menu>
    <audio data-ref="soundAccept"></audio>
    <audio data-ref="soundOpen"></audio>
  </form>`

  document.body.appendChild(this.dialog)

  // ...
}

检查支持

浏览器支持<dialog>的道路很长。Safari最近才支持它。火狐甚至也是最近,虽然不是<form method="dialog">一部分。因此,我们需要添加type="button"我们正在模仿的接受取消按钮。否则,它们将POST表单并导致页面刷新,我们希望避免这种情况。

<button${this.dialogSupported ? '' : ` type="button"`}...></button>

DOM节点引用

你注意到所有的data-ref-attribuites了吗?我们将使用这些来获取对DOM节点的引用:

this.elements = {}
this.dialog.querySelectorAll('[data-ref]').forEach(el => this.elements[el.dataset.ref] = el)

到目前为止,this.elements.accept引用的是“Accept”按钮,而this.elements.cancel引用的是“Cancel”按钮。

按钮属性

对于屏幕阅读器,我们需要一个指向描述对话框的标签IDaria-labelledby属性——<legend>标签,它将包含message

this.dialog.setAttribute('aria-labelledby', this.elements.message.id)

id?这是对这部分<legend>元素的唯一引用:

取消按钮

好消息!HTML对话框元素有一个内置cancel()方法,可以更轻松地替换调用该confirm()方法的JavaScript对话框。让我们在单击取消按钮时发出该事件:

this.elements.cancel.addEventListener('click', () => { 
  this.dialog.dispatchEvent(new Event('cancel')) 
})

这是我们<dialog>替换alert(),confirm()prompt()的框架。

填充不支持的浏览器

我们需要为不支持它的浏览器隐藏HTML对话框元素。为此,我们将在一个新toggle()方法中包装显示和隐藏对话框的逻辑:

toggle(open = false) {
  if (this.dialogSupported && open) this.dialog.showModal()
  if (!this.dialogSupported) {
    document.body.classList.toggle(this.settings.bodyClass, open)
    this.dialog.hidden = !open
    /* If a `target` exists, set focus on it when closing */
    if (this.elements.target && !open) {
      this.elements.target.focus()
    }
  }
}
/* Then call it at the end of `init`: */
this.toggle()

键盘导航

接下来,让我们实现一种捕获焦点的方法,以便用户可以在对话框中的按钮之间切换,而不会无意中退出对话框。有很多方法可以做到这一点。我喜欢CSS 方式,但不幸的是,它不可靠。相反,让我们从对话框中获取所有可聚焦元素作为NodeList并将其存储在this.focusable

getFocusable() {
  return [...this.dialog.querySelectorAll('button,[href],select,textarea,input:not([type=&quot;hidden&quot;]),[tabindex]:not([tabindex=&quot;-1&quot;])')]
}

接下来,我们将添加一个keydown事件监听器,处理我们所有的键盘导航逻辑:

this.dialog.addEventListener('keydown', e => {
  if (e.key === 'Enter') {
    if (!this.dialogSupported) e.preventDefault()
    this.elements.accept.dispatchEvent(new Event('click'))
  }
  if (e.key === 'Escape') this.dialog.dispatchEvent(new Event('cancel'))
  if (e.key === 'Tab') {
    e.preventDefault()
    const len =  this.focusable.length - 1;
    let index = this.focusable.indexOf(e.target);
    index = e.shiftKey ? index-1 : index+1;
    if (index < 0) index = len;
    if (index > len) index = 0;
    this.focusable[index].focus();
  }
})

对于Enter,我们需要防止在不支持该<form>元素的浏览器中提交。将发出一个事件。按Tab键将在可聚焦元素的节点列表中找到当前元素,并将焦点设置在下一项(如果同时按住Shift键,则为上一项)。

显示<dialog>

现在让我们显示对话框!为此,我们需要一个将可选settings对象与默认值合并的小方法。在这个对象中——就像默认settings对象一样——我们可以添加或更改特定对话框的设置。

open(settings = {}) {
  const dialog = Object.assign({}, this.settings, settings)
  this.dialog.className = dialog.dialogClass || ''

  /* set innerText of the elements */
  this.elements.accept.innerText = dialog.accept
  this.elements.cancel.innerText = dialog.cancel
  this.elements.cancel.hidden = dialog.cancel === ''
  this.elements.message.innerText = dialog.message

  /* If sounds exists, update `src` */
  this.elements.soundAccept.src = dialog.soundAccept || ''
  this.elements.soundOpen.src = dialog.soundOpen || ''

  /* A target can be added (from the element invoking the dialog */
  this.elements.target = dialog.target || ''

  /* Optional HTML for custom dialogs */
  this.elements.template.innerHTML = dialog.template || ''

  /* Grab focusable elements */
  this.focusable = this.getFocusable()
  this.hasFormData = this.elements.fieldset.elements.length > 0
  if (dialog.soundOpen) {
    this.elements.soundOpen.play()
  }
  this.toggle(true)
  if (this.hasFormData) {
    /* If form elements exist, focus on that first */
    this.focusable[0].focus()
    this.focusable[0].select()
  }
  else {
    this.elements.accept.focus()
  }
}

那是很多代码。现在我们可以在所有浏览器中显示该<dialog>元素。但是我们仍然需要模仿执行后等待用户输入的功能,比如原生的alert()confirm()prompt()方法。为此,我们需要一个Promise我正在调用的新waitForUser()方法:

waitForUser() {
  return new Promise(resolve => {
    this.dialog.addEventListener('cancel', () => { 
      this.toggle()
      resolve(false)
    }, { once: true })
    this.elements.accept.addEventListener('click', () => {
      let value = this.hasFormData ? 
        this.collectFormData(new FormData(this.elements.form)) : true;
      if (this.elements.soundAccept.src) this.elements.soundAccept.play()
      this.toggle()
      resolve(value)
    }, { once: true })
  })
}

此方法返回一个Promise。在其中,我们为取消接受添加事件侦听器,它们可以解析false(取消)或true(接受)。如果formData存在(对于自定义对话框或prompt),这些将使用辅助方法收集,然后在对象中返回:

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
}

我们可以立即删除事件监听器,使用{ once: true }

注:为了简单起见,我不使用reject(),而是简单地使用resolve false

隐藏<dialog>

早些时候,我们为内置cancel事件添加了事件侦听器。当用户单击取消按钮Escape键时,我们调用此事件。该cancel事件删除<dialog>上的open属性,从而隐藏它。

去哪里:focus

在我们的open()方法中,我们关注第一个可聚焦的表单字段接受按钮:

if (this.hasFormData) {
  this.focusable[0].focus()
  this.focusable[0].select()
}
else {
  this.elements.accept.focus()
}

但这是正确的吗?在W3的“Modal Dialog”示例中,确实如此。不过,在Scott Ohara的示例中,重点是对话框本身——如果屏幕阅读器应该阅读我们之前在aria-labelledby属性中定义的文本,这是有道理的。我不确定哪个是正确的或最好的,但如果我们想使用Scott的方法。我们需要在我们的init方法中添加一个tabindex="-1"<dialog>

this.dialog.tabIndex = -1

然后,在open()方法中,我们将焦点代码替换为:

this.dialog.focus()

我们可以在DevTools中的任何给定时间通过单击眼睛图标并在控制台中输入document.activeElement来检查activeElement(具有焦点的元素) 。尝试四处切换以查看它的更新:

 

添加警报、确认和提示

我们终于准备好将alert(),confirm()prompt()添加到我们的Dialog类中了。这些将是替代JavaScript对话框和这些方法的原始语法的小型辅助方法。它们都调用我们之前创建的open()方法,但使用的settings对象与我们触发原始方法的方式相匹配。

让我们与原始语法进行比较。

alert()通常是这样触发的:

window.alert(message);

在我们的对话框中,我们将添加一个模仿这个的alert()方法:

/* dialog.alert() */
alert(message, config = { target: event.target }) {
  const settings = Object.assign({}, config, { cancel: '', message, template: '' })
  this.open(settings)
  return this.waitForUser()
}

我们将cancelandtemplate设置为空字符串,这样——即使我们之前已经设置了默认值——这些也不会被隐藏,而只会messageaccept被显示。

confirm()通常是这样触发的:

window.confirm(message);

在我们的版本中,与alert()类似,我们创建了一个自定义方法来显示messagecancelaccept项:

/* dialog.confirm() */
confirm(message, config = { target: event.target }) {
  const settings = Object.assign({}, config, { message, template: '' })
  this.open(settings)
  return this.waitForUser()
}

prompt()通常是这样触发的:

window.prompt(message, default);

在这里,我们需要添加一个带有<input>template,我们将它包装在 <label>

/* dialog.prompt() */
prompt(message, value, config = { target: event.target }) {
  const template = `
  <label aria-label="${message}">
    <input name="prompt" value="${value}">
  </label>`
  const settings = Object.assign({}, config, { message, template })
  this.open(settings)
  return this.waitForUser()
}

注:{ target: event.target }是对调用该方法的DOM元素的引用。当我们关闭 时,我们将使用它来重新关注该<dialog>元素,将用户返回到触发对话框之前的位置。

我们应该测试一下

是时候测试并确保一切都按预期工作了。让我们创建一个新的HTML文件,导入类,然后创建一个实例:

<script type="module">
  import Dialog from './dialog.js';
  const dialog = new Dialog();
</script>

一次尝试以下用例!

/* alert */
dialog.alert('Please refresh your browser')
/* or */
dialog.alert('Please refresh your browser').then((res) => {  console.log(res) })

/* confirm */
dialog.confirm('Do you want to continue?').then((res) => { console.log(res) })

/* prompt */
dialog.prompt('The meaning of life?', 42).then((res) => { console.log(res) })

然后在单击接受取消时观看控制台。请在按EscapeEnter键时重试。

异步/等待

我们也可以使用async/await这样做的方式。我们通过模仿原始语法来更多地替换JavaScript对话框,但它需要包装函数是async,而其中的代码需要await关键字:

document.getElementById('promptButton').addEventListener('click', async (e) => {
  const value = await dialog.prompt('The meaning of life?', 42);
  console.log(value);
});

跨浏览器样式

我们现在拥有一个功能齐全的跨浏览器和屏幕阅读器友好的HTML对话框元素,它取代了JavaScript对话框!我们已经介绍了很多。但造型可以用很多的爱。让我们利用现有的data-componentdata-ref-attributes 来添加跨浏览器样式——不需要额外的类或其他属性!

我们将使用CSS:where伪选择器来保持我们的默认样式不受特殊性影响:

:where([data-component*="dialog"] *) {  
  box-sizing: border-box;
  outline-color: var(--dlg-outline-c, hsl(218, 79.19%, 35%))
}
:where([data-component*="dialog"]) {
  --dlg-gap: 1em;
  background: var(--dlg-bg, #fff);
  border: var(--dlg-b, 0);
  border-radius: var(--dlg-bdrs, 0.25em);
  box-shadow: var(--dlg-bxsh, 0px 25px 50px -12px rgba(0, 0, 0, 0.25));
  font-family:var(--dlg-ff, ui-sansserif, system-ui, sans-serif);
  min-inline-size: var(--dlg-mis, auto);
  padding: var(--dlg-p, var(--dlg-gap));
  width: var(--dlg-w, fit-content);
}
:where([data-component="no-dialog"]:not([hidden])) {
  display: block;
  inset-block-start: var(--dlg-gap);
  inset-inline-start: 50%;
  position: fixed;
  transform: translateX(-50%);
}
:where([data-component*="dialog"] menu) {
  display: flex;
  gap: calc(var(--dlg-gap) / 2);
  justify-content: var(--dlg-menu-jc, flex-end);
  margin: 0;
  padding: 0;
}
:where([data-component*="dialog"] menu button) {
  background-color: var(--dlg-button-bgc);
  border: 0;
  border-radius: var(--dlg-bdrs, 0.25em);
  color: var(--dlg-button-c);
  font-size: var(--dlg-button-fz, 0.8em);
  padding: var(--dlg-button-p, 0.65em 1.5em);
}
:where([data-component*="dialog"] [data-ref="accept"]) {
  --dlg-button-bgc: var(--dlg-accept-bgc, hsl(218, 79.19%, 46.08%));
  --dlg-button-c: var(--dlg-accept-c, #fff);
}
:where([data-component*="dialog"] [data-ref="cancel"]) {
  --dlg-button-bgc: var(--dlg-cancel-bgc, transparent);
  --dlg-button-c: var(--dlg-cancel-c, inherit);
}
:where([data-component*="dialog"] [data-ref="fieldset"]) {
  border: 0;
  margin: unset;
  padding: unset;
}
:where([data-component*="dialog"] [data-ref="message"]) {
  font-size: var(--dlg-message-fz, 1.25em);
  margin-block-end: var(--dlg-gap);
}
:where([data-component*="dialog"] [data-ref="template"]:not(:empty)) {
  margin-block-end: var(--dlg-gap);
  width: 100%;
}

当然,您可以根据需要设计这些样式。以下是上述CSS将为您提供的内容:

alert()

confirm()

prompt()

要覆盖这些样式并使用您自己的样式,请在dialogClass中添加一个类,

dialogClass: 'custom'

然后在CSS中添加类,并更新CSS自定义属性值:

.custom {
  --dlg-accept-bgc: hsl(159, 65%, 75%);
  --dlg-accept-c: #000;
  /* etc. */
}

自定义对话框示例

如果我们模仿的标准alert()confirm()prompt()方法无法满足您的特定用例怎么办?实际上,我们可以做更多的事情来使其<dialog>更灵活,以涵盖比我们迄今为止所涵盖的内容、按钮和功能更多的内容——而且工作量也不大。

早些时候,我曾讨论过在对话中添加声音的想法。让我们这样做。

您可以使用settings对象的template属性来注入更多的HTML。这是一个自定义示例,从带有id="btnCustom"<button>调用,从MP3文件中触发一个有趣的小声音:

document.getElementById('btnCustom').addEventListener('click', (e) => {
  dialog.open({
    accept: 'Sign in',
    dialogClass: 'custom',
    message: 'Please enter your credentials',
    soundAccept: 'https://assets.yourdomain.com/accept.mp3',
    soundOpen: 'https://assets.yourdomain.com/open.mp3',
    target: e.target,
    template: `
    <label>Username<input type="text" name="username" value="admin"></label>
    <label>Password<input type="password" name="password" value="password"></label>`
  })
  dialog.waitForUser().then((res) => {  console.log(res) })
});

现场演示

这是一支包含我们构建的所有东西的钢笔!打开控制台,单击按钮,然后玩弄对话框,单击按钮并使用键盘接受和取消。

所以你怎么看?这是用较新的HTML对话框元素替换JavaScript对话框的好方法吗?或者你有没有尝试过另一种方式?在评论中告诉我!

https://css-tricks.com/replace-javascript-dialogs-html-dialog-element/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值