-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
) * 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
- Loading branch information
1 parent
0505e92
commit 67f3c4d
Showing
14 changed files
with
447 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}) | ||
} |
95 changes: 95 additions & 0 deletions
95
packages/@headlessui-react/src/utils/get-text-value.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
Oops, something went wrong.
67f3c4d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
headlessui-vue – ./packages/playground-vue
headlessui-vue-git-main-tailwindlabs.vercel.app
headlessui-vue-tailwindlabs.vercel.app
headlessui-vue.vercel.app
67f3c4d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
headlessui-react – ./packages/playground-react
headlessui-react-tailwindlabs.vercel.app
headlessui-react.vercel.app
headlessui-react-git-main-tailwindlabs.vercel.app