Improve control over Menu and Listbox options while searching (#2471)
* add `get-text-value` helper * use `getTextValue` in `Listbox` component * use `getTextValue` in `Menu` component * update changelog * ensure we handle multiple values for `aria-labelledby` * hoist regex * drop child nodes instead of replacing its innerText This makes it a bit slower but also more correct. We can use a cache on another level to ensure that we are not creating useless work. * add `useTextValue` to improve performance of `getTextValue` This will add a cache and only if the `innerText` changes, only then will we calculate the new text value. * use better `useTextValue` hook
This commit is contained in:
@@ -3,7 +3,11 @@ module.exports = function createJestConfig(root, options) {
|
||||
return Object.assign(
|
||||
{
|
||||
rootDir: root,
|
||||
setupFilesAfterEnv: ['<rootDir>../../jest/custom-matchers.ts', ...setupFilesAfterEnv],
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>../../jest/custom-matchers.ts',
|
||||
'<rootDir>../../jest/polyfills.ts',
|
||||
...setupFilesAfterEnv,
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': '@swc/jest',
|
||||
...transform,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// JSDOM Doesn't implement innerText yet: https://github.com/jsdom/jsdom/issues/1245
|
||||
// So this is a hacky way of implementing it using `textContent`.
|
||||
// Real implementation doesn't use textContent because:
|
||||
// > textContent gets the content of all elements, including <script> and <style> elements. In
|
||||
// > contrast, innerText only shows "human-readable" elements.
|
||||
// >
|
||||
// > — https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext
|
||||
Object.defineProperty(HTMLElement.prototype, 'innerText', {
|
||||
get() {
|
||||
return this.textContent
|
||||
},
|
||||
set(value) {
|
||||
this.textContent = value
|
||||
},
|
||||
})
|
||||
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Ensure `FocusTrap` is only active when the given `enabled` value is `true` ([#2456](https://github.com/tailwindlabs/headlessui/pull/2456))
|
||||
- Stop `<Transition appear>` from overwriting classes on re-render ([#2457](https://github.com/tailwindlabs/headlessui/pull/2457))
|
||||
- Improve control over `Menu` and `Listbox` options while searching ([#2471](https://github.com/tailwindlabs/headlessui/pull/2471))
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import { useEvent } from '../../hooks/use-event'
|
||||
import { useControllable } from '../../hooks/use-controllable'
|
||||
import { useLatestValue } from '../../hooks/use-latest-value'
|
||||
import { useTrackedPointer } from '../../hooks/use-tracked-pointer'
|
||||
import { useTextValue } from '../../hooks/use-text-value'
|
||||
|
||||
enum ListboxStates {
|
||||
Open,
|
||||
@@ -934,12 +935,13 @@ function OptionFn<
|
||||
|
||||
let selected = data.isSelected(value)
|
||||
let internalOptionRef = useRef<HTMLLIElement | null>(null)
|
||||
let getTextValue = useTextValue(internalOptionRef)
|
||||
let bag = useLatestValue<ListboxOptionDataRef<TType>['current']>({
|
||||
disabled,
|
||||
value,
|
||||
domRef: internalOptionRef,
|
||||
get textValue() {
|
||||
return internalOptionRef.current?.textContent?.toLowerCase()
|
||||
return getTextValue()
|
||||
},
|
||||
})
|
||||
let optionRef = useSyncRefs(ref, internalOptionRef)
|
||||
|
||||
@@ -51,6 +51,7 @@ import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
|
||||
import { useOwnerDocument } from '../../hooks/use-owner'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
import { useTrackedPointer } from '../../hooks/use-tracked-pointer'
|
||||
import { useTextValue } from '../../hooks/use-text-value'
|
||||
|
||||
enum MenuStates {
|
||||
Open,
|
||||
@@ -636,14 +637,19 @@ function ItemFn<TTag extends ElementType = typeof DEFAULT_ITEM_TAG>(
|
||||
/* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ state.activeItemIndex,
|
||||
])
|
||||
|
||||
let bag = useRef<MenuItemDataRef['current']>({ disabled, domRef: internalItemRef })
|
||||
let getTextValue = useTextValue(internalItemRef)
|
||||
|
||||
let bag = useRef<MenuItemDataRef['current']>({
|
||||
disabled,
|
||||
domRef: internalItemRef,
|
||||
get textValue() {
|
||||
return getTextValue()
|
||||
},
|
||||
})
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
bag.current.disabled = disabled
|
||||
}, [bag, disabled])
|
||||
useIsoMorphicEffect(() => {
|
||||
bag.current.textValue = internalItemRef.current?.textContent?.toLowerCase()
|
||||
}, [bag, internalItemRef])
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
dispatch({ type: ActionTypes.RegisterItem, id, dataRef: bag })
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useRef, MutableRefObject } from 'react'
|
||||
import { getTextValue } from '../utils/get-text-value'
|
||||
import { useEvent } from './use-event'
|
||||
|
||||
export function useTextValue(element: MutableRefObject<HTMLElement | null>) {
|
||||
let cacheKey = useRef<string>('')
|
||||
let cacheValue = useRef<string>('')
|
||||
|
||||
return useEvent(() => {
|
||||
let el = element.current
|
||||
if (!el) return ''
|
||||
|
||||
// Check for a cached version
|
||||
let currentKey = el.innerText
|
||||
if (cacheKey.current === currentKey) {
|
||||
return cacheValue.current
|
||||
}
|
||||
|
||||
// Calculate the value
|
||||
let value = getTextValue(el).trim().toLowerCase()
|
||||
cacheKey.current = currentKey
|
||||
cacheValue.current = value
|
||||
return value
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { getTextValue } from './get-text-value'
|
||||
|
||||
let html = String.raw
|
||||
|
||||
it('should be possible to get the text value from an element', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerText = 'Hello World'
|
||||
expect(getTextValue(element)).toEqual('Hello World')
|
||||
})
|
||||
|
||||
it('should strip out emojis when receiving the text from the element', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerText = '🇨🇦 Canada'
|
||||
expect(getTextValue(element)).toEqual('Canada')
|
||||
})
|
||||
|
||||
it('should strip out hidden elements', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerHTML = html`<div><span hidden>Hello</span> world</div>`
|
||||
expect(getTextValue(element)).toEqual('world')
|
||||
})
|
||||
|
||||
it('should strip out aria-hidden elements', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerHTML = html`<div><span aria-hidden>Hello</span> world</div>`
|
||||
expect(getTextValue(element)).toEqual('world')
|
||||
})
|
||||
|
||||
it('should strip out role="img" elements', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerHTML = html`<div><span role="img">°</span> world</div>`
|
||||
expect(getTextValue(element)).toEqual('world')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the aria-label', () => {
|
||||
let element = document.createElement('div')
|
||||
element.setAttribute('aria-label', 'Hello World')
|
||||
expect(getTextValue(element)).toEqual('Hello World')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the aria-label (even if there is content)', () => {
|
||||
let element = document.createElement('div')
|
||||
element.setAttribute('aria-label', 'Hello World')
|
||||
element.innerHTML = 'Hello Universe'
|
||||
element.innerText = 'Hello Universe'
|
||||
expect(getTextValue(element)).toEqual('Hello World')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the element referenced by aria-labelledby (using `aria-label`)', () => {
|
||||
document.body.innerHTML = html`
|
||||
<div>
|
||||
<div id="foo" aria-labelledby="bar">Contents of foo</div>
|
||||
<div id="bar" aria-label="Actual value of bar">Contents of bar</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
expect(getTextValue(document.querySelector('#foo')!)).toEqual('Actual value of bar')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the element referenced by aria-labelledby (using its contents)', () => {
|
||||
document.body.innerHTML = html`
|
||||
<div>
|
||||
<div id="foo" aria-labelledby="bar">Contents of foo</div>
|
||||
<div id="bar">Contents of bar</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
expect(getTextValue(document.querySelector('#foo')!)).toEqual('Contents of bar')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the element referenced by aria-labelledby (using `aria-label`, multiple)', () => {
|
||||
document.body.innerHTML = html`
|
||||
<div>
|
||||
<div id="foo" aria-labelledby="bar baz">Contents of foo</div>
|
||||
<div id="bar" aria-label="Actual value of bar">Contents of bar</div>
|
||||
<div id="baz" aria-label="Actual value of baz">Contents of baz</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
expect(getTextValue(document.querySelector('#foo')!)).toEqual(
|
||||
'Actual value of bar, Actual value of baz'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the element referenced by aria-labelledby (using its contents, multiple)', () => {
|
||||
document.body.innerHTML = html`
|
||||
<div>
|
||||
<div id="foo" aria-labelledby="bar baz">Contents of foo</div>
|
||||
<div id="bar">Contents of bar</div>
|
||||
<div id="baz">Contents of baz</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
expect(getTextValue(document.querySelector('#foo')!)).toEqual('Contents of bar, Contents of baz')
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
let emojiRegex =
|
||||
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g
|
||||
|
||||
function getTextContents(element: HTMLElement): string {
|
||||
// Using innerText instead of textContent because:
|
||||
//
|
||||
// > textContent gets the content of all elements, including <script> and <style> elements. In
|
||||
// > contrast, innerText only shows "human-readable" elements.
|
||||
// >
|
||||
// > — https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext
|
||||
let currentInnerText = element.innerText ?? ''
|
||||
|
||||
// Remove all the elements that shouldn't be there.
|
||||
//
|
||||
// [hidden] — The user doesn't see it
|
||||
// [aria-hidden] — The screen reader doesn't see it
|
||||
// [role="img"] — Even if it is text, it is used as an image
|
||||
//
|
||||
// This is probably the slowest part, but if you want complete control over the text value, then
|
||||
// it is better to set an `aria-label` instead.
|
||||
let copy = element.cloneNode(true)
|
||||
if (!(copy instanceof HTMLElement)) {
|
||||
return currentInnerText
|
||||
}
|
||||
|
||||
let dropped = false
|
||||
// Drop the elements that shouldn't be there.
|
||||
for (let child of copy.querySelectorAll('[hidden],[aria-hidden],[role="img"]')) {
|
||||
child.remove()
|
||||
dropped = true
|
||||
}
|
||||
|
||||
// Now that the elements are removed, we can get the innerText such that we can strip the emojis.
|
||||
let value = dropped ? copy.innerText ?? '' : currentInnerText
|
||||
|
||||
// Check if it contains some emojis or not, if so, we need to remove them
|
||||
// because ideally we work with simple text values.
|
||||
//
|
||||
// Ideally we can use the much simpler RegEx: /\p{Extended_Pictographic}/u
|
||||
// but we can't rely on this yet, so we use the more complex one.
|
||||
if (emojiRegex.test(value)) {
|
||||
value = value.replace(emojiRegex, '')
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function getTextValue(element: HTMLElement): string {
|
||||
// Try to use the `aria-label` first
|
||||
let label = element.getAttribute('aria-label')
|
||||
if (typeof label === 'string') return label.trim()
|
||||
|
||||
// Try to use the `aria-labelledby` second
|
||||
let labelledby = element.getAttribute('aria-labelledby')
|
||||
if (labelledby) {
|
||||
// aria-labelledby can be a space-separated list of IDs, so we need to split them up and
|
||||
// combine them into a single string.
|
||||
let labels = labelledby
|
||||
.split(' ')
|
||||
.map((labelledby) => {
|
||||
let labelEl = document.getElementById(labelledby)
|
||||
if (labelEl) {
|
||||
let label = labelEl.getAttribute('aria-label')
|
||||
// Try to use the `aria-label` first (of the referenced element)
|
||||
if (typeof label === 'string') return label.trim()
|
||||
|
||||
// This time, the `aria-labelledby` isn't used anymore (in Safari), so we just have to
|
||||
// look at the contents itself.
|
||||
return getTextContents(labelEl).trim()
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
if (labels.length > 0) return labels.join(', ')
|
||||
}
|
||||
|
||||
// Try to use the text contents of the element itself
|
||||
return getTextContents(element).trim()
|
||||
}
|
||||
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fix memory leak in `Popover` component ([#2430](https://github.com/tailwindlabs/headlessui/pull/2430))
|
||||
- Ensure `FocusTrap` is only active when the given `enabled` value is `true` ([#2456](https://github.com/tailwindlabs/headlessui/pull/2456))
|
||||
- Ensure the exposed `activeIndex` is up to date for the `Combobox` component ([#2463](https://github.com/tailwindlabs/headlessui/pull/2463))
|
||||
- Improve control over `Menu` and `Listbox` options while searching ([#2471](https://github.com/tailwindlabs/headlessui/pull/2471))
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import { Hidden, Features as HiddenFeatures } from '../../internal/hidden'
|
||||
import { objectToFormEntries } from '../../utils/form'
|
||||
import { useControllable } from '../../hooks/use-controllable'
|
||||
import { useTrackedPointer } from '../../hooks/use-tracked-pointer'
|
||||
import { useTextValue } from '../../hooks/use-text-value'
|
||||
|
||||
function defaultComparator<T>(a: T, z: T): boolean {
|
||||
return a === z
|
||||
@@ -731,16 +732,15 @@ export let ListboxOption = defineComponent({
|
||||
})
|
||||
})
|
||||
|
||||
let getTextValue = useTextValue(internalOptionRef)
|
||||
let dataRef = computed<ListboxOptionData>(() => ({
|
||||
disabled: props.disabled,
|
||||
value: props.value,
|
||||
textValue: '',
|
||||
get textValue() {
|
||||
return getTextValue()
|
||||
},
|
||||
domRef: internalOptionRef,
|
||||
}))
|
||||
onMounted(() => {
|
||||
let textValue = dom(internalOptionRef)?.textContent?.toLowerCase().trim()
|
||||
if (textValue !== undefined) dataRef.value.textValue = textValue
|
||||
})
|
||||
|
||||
onMounted(() => api.registerOption(props.id, dataRef))
|
||||
onUnmounted(() => api.unregisterOption(props.id))
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from '../../utils/focus-management'
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click'
|
||||
import { useTrackedPointer } from '../../hooks/use-tracked-pointer'
|
||||
import { useTextValue } from '../../hooks/use-text-value'
|
||||
|
||||
enum MenuStates {
|
||||
Open,
|
||||
@@ -511,15 +512,14 @@ export let MenuItem = defineComponent({
|
||||
: false
|
||||
})
|
||||
|
||||
let getTextValue = useTextValue(internalItemRef)
|
||||
let dataRef = computed<MenuItemData>(() => ({
|
||||
disabled: props.disabled,
|
||||
textValue: '',
|
||||
get textValue() {
|
||||
return getTextValue()
|
||||
},
|
||||
domRef: internalItemRef,
|
||||
}))
|
||||
onMounted(() => {
|
||||
let textValue = dom(internalItemRef)?.textContent?.toLowerCase().trim()
|
||||
if (textValue !== undefined) dataRef.value.textValue = textValue
|
||||
})
|
||||
|
||||
onMounted(() => api.registerItem(props.id, dataRef))
|
||||
onUnmounted(() => api.unregisterItem(props.id))
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ref, Ref } from 'vue'
|
||||
import { getTextValue } from '../utils/get-text-value'
|
||||
import { dom } from '../utils/dom'
|
||||
|
||||
export function useTextValue(element: Ref<HTMLElement | null>) {
|
||||
let cacheKey = ref<string>('')
|
||||
let cacheValue = ref<string>('')
|
||||
|
||||
return () => {
|
||||
let el = dom(element)
|
||||
if (!el) return ''
|
||||
|
||||
// Check for a cached version
|
||||
let currentKey = el.innerText
|
||||
if (cacheKey.value === currentKey) {
|
||||
return cacheValue.value
|
||||
}
|
||||
|
||||
// Calculate the value
|
||||
let value = getTextValue(el).trim().toLowerCase()
|
||||
cacheKey.value = currentKey
|
||||
cacheValue.value = value
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { getTextValue } from './get-text-value'
|
||||
|
||||
let html = String.raw
|
||||
|
||||
it('should be possible to get the text value from an element', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerText = 'Hello World'
|
||||
expect(getTextValue(element)).toEqual('Hello World')
|
||||
})
|
||||
|
||||
it('should strip out emojis when receiving the text from the element', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerText = '🇨🇦 Canada'
|
||||
expect(getTextValue(element)).toEqual('Canada')
|
||||
})
|
||||
|
||||
it('should strip out hidden elements', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerHTML = html`<div><span hidden>Hello</span> world</div>`
|
||||
expect(getTextValue(element)).toEqual('world')
|
||||
})
|
||||
|
||||
it('should strip out aria-hidden elements', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerHTML = html`<div><span aria-hidden>Hello</span> world</div>`
|
||||
expect(getTextValue(element)).toEqual('world')
|
||||
})
|
||||
|
||||
it('should strip out role="img" elements', () => {
|
||||
let element = document.createElement('div')
|
||||
element.innerHTML = html`<div><span role="img">°</span> world</div>`
|
||||
expect(getTextValue(element)).toEqual('world')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the aria-label', () => {
|
||||
let element = document.createElement('div')
|
||||
element.setAttribute('aria-label', 'Hello World')
|
||||
expect(getTextValue(element)).toEqual('Hello World')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the aria-label (even if there is content)', () => {
|
||||
let element = document.createElement('div')
|
||||
element.setAttribute('aria-label', 'Hello World')
|
||||
element.innerHTML = 'Hello Universe'
|
||||
element.innerText = 'Hello Universe'
|
||||
expect(getTextValue(element)).toEqual('Hello World')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the element referenced by aria-labelledby (using `aria-label`)', () => {
|
||||
document.body.innerHTML = html`
|
||||
<div>
|
||||
<div id="foo" aria-labelledby="bar">Contents of foo</div>
|
||||
<div id="bar" aria-label="Actual value of bar">Contents of bar</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
expect(getTextValue(document.querySelector('#foo')!)).toEqual('Actual value of bar')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the element referenced by aria-labelledby (using its contents)', () => {
|
||||
document.body.innerHTML = html`
|
||||
<div>
|
||||
<div id="foo" aria-labelledby="bar">Contents of foo</div>
|
||||
<div id="bar">Contents of bar</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
expect(getTextValue(document.querySelector('#foo')!)).toEqual('Contents of bar')
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the element referenced by aria-labelledby (using `aria-label`, multiple)', () => {
|
||||
document.body.innerHTML = html`
|
||||
<div>
|
||||
<div id="foo" aria-labelledby="bar baz">Contents of foo</div>
|
||||
<div id="bar" aria-label="Actual value of bar">Contents of bar</div>
|
||||
<div id="baz" aria-label="Actual value of baz">Contents of baz</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
expect(getTextValue(document.querySelector('#foo')!)).toEqual(
|
||||
'Actual value of bar, Actual value of baz'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be possible to get the text value from the element referenced by aria-labelledby (using its contents, multiple)', () => {
|
||||
document.body.innerHTML = html`
|
||||
<div>
|
||||
<div id="foo" aria-labelledby="bar baz">Contents of foo</div>
|
||||
<div id="bar">Contents of bar</div>
|
||||
<div id="baz">Contents of baz</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
expect(getTextValue(document.querySelector('#foo')!)).toEqual('Contents of bar, Contents of baz')
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
let emojiRegex =
|
||||
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g
|
||||
|
||||
function getTextContents(element: HTMLElement): string {
|
||||
// Using innerText instead of textContent because:
|
||||
//
|
||||
// > textContent gets the content of all elements, including <script> and <style> elements. In
|
||||
// > contrast, innerText only shows "human-readable" elements.
|
||||
// >
|
||||
// > — https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext
|
||||
let currentInnerText = element.innerText ?? ''
|
||||
|
||||
// Remove all the elements that shouldn't be there.
|
||||
//
|
||||
// [hidden] — The user doesn't see it
|
||||
// [aria-hidden] — The screen reader doesn't see it
|
||||
// [role="img"] — Even if it is text, it is used as an image
|
||||
//
|
||||
// This is probably the slowest part, but if you want complete control over the text value, then
|
||||
// it is better to set an `aria-label` instead.
|
||||
let copy = element.cloneNode(true)
|
||||
if (!(copy instanceof HTMLElement)) {
|
||||
return currentInnerText
|
||||
}
|
||||
|
||||
let dropped = false
|
||||
// Drop the elements that shouldn't be there.
|
||||
for (let child of copy.querySelectorAll('[hidden],[aria-hidden],[role="img"]')) {
|
||||
child.remove()
|
||||
dropped = true
|
||||
}
|
||||
|
||||
// Now that the elements are removed, we can get the innerText such that we can strip the emojis.
|
||||
let value = dropped ? copy.innerText ?? '' : currentInnerText
|
||||
|
||||
// Check if it contains some emojis or not, if so, we need to remove them
|
||||
// because ideally we work with simple text values.
|
||||
//
|
||||
// Ideally we can use the much simpler RegEx: /\p{Extended_Pictographic}/u
|
||||
// but we can't rely on this yet, so we use the more complex one.
|
||||
if (emojiRegex.test(value)) {
|
||||
value = value.replace(emojiRegex, '')
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function getTextValue(element: HTMLElement): string {
|
||||
// Try to use the `aria-label` first
|
||||
let label = element.getAttribute('aria-label')
|
||||
if (typeof label === 'string') return label.trim()
|
||||
|
||||
// Try to use the `aria-labelledby` second
|
||||
let labelledby = element.getAttribute('aria-labelledby')
|
||||
if (labelledby) {
|
||||
// aria-labelledby can be a space-separated list of IDs, so we need to split them up and
|
||||
// combine them into a single string.
|
||||
let labels = labelledby
|
||||
.split(' ')
|
||||
.map((labelledby) => {
|
||||
let labelEl = document.getElementById(labelledby)
|
||||
if (labelEl) {
|
||||
let label = labelEl.getAttribute('aria-label')
|
||||
// Try to use the `aria-label` first (of the referenced element)
|
||||
if (typeof label === 'string') return label.trim()
|
||||
|
||||
// This time, the `aria-labelledby` isn't used anymore (in Safari), so we just have to
|
||||
// look at the contents itself.
|
||||
return getTextContents(labelEl).trim()
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
if (labels.length > 0) return labels.join(', ')
|
||||
}
|
||||
|
||||
// Try to use the text contents of the element itself
|
||||
return getTextContents(element).trim()
|
||||
}
|
||||
Reference in New Issue
Block a user