Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/picker/components/Picker/PickerTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export function render (container, state, helpers, events, actions, refs, abortS
}
</div>
<!-- This serves as a baseline emoji for measuring against and determining emoji support -->
<!-- it must match BASELINE_EMOJI in baselineEmoji.js - inlined to avoid unnecessary templating perf tax -->
<button data-ref="baselineEmoji" aria-hidden="true" tabindex="-1" class="abs-pos hidden emoji baseline-emoji">
😀
</button>
Expand Down
1 change: 1 addition & 0 deletions src/picker/utils/baselineEmoji.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const BASELINE_EMOJI = '😀'
55 changes: 37 additions & 18 deletions src/picker/utils/checkZwjSupport.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,57 @@
import { calculateTextWidth } from './calculateTextWidth'
import { supportedZwjEmojis } from './emojiSupport'
import { BASELINE_EMOJI } from './baselineEmoji.js'

let baselineEmojiWidth
let fallbackNode

// only used in tests
let simulateBrowserNotSupportingZWJEmoji = false
export function setSimulateBrowserNotSupportingZWJEmoji (value) {
simulateBrowserNotSupportingZWJEmoji = value
}

function calculateTextWidthWithFallback (unicode, domNode, baselineEmojiNode) {
const result = calculateTextWidth(domNode)
/* istanbul ignore if */
if (!result) {
// If result is 0 then very likely the emoji-picker has `display:none` or equivalent. In that case, we fall back to
// cloning the baseline emoji, putting that in the `document.body`, and measuring that instead. This is a perf hit,
// but it's better than mistakenly filtering emoji: https://github.com/nolanlawson/emoji-picker-element/issues/514
console.log('Picker is hidden, using fallback node for width calculation', unicode)
if (!fallbackNode) {
fallbackNode = baselineEmojiNode.cloneNode(true)
// We have to copy styles because we're copying from an element in the shadow DOM to the light DOM
// We can't use the shadow DOM because it's likely the entire picker is `display:none`
const styles = getComputedStyle(baselineEmojiNode)
// probably don't need display/align-items/justify-content but let's play it safe
for (const prop of ['font-family', 'line-height', 'width', 'height', 'font-size', 'display', 'align-items', 'justify-content']) {
// set `!important` just in case some global styles might try to overwrite this
fallbackNode.style.setProperty(prop, styles.getPropertyValue(prop), 'important')
}
}
try {
document.body.appendChild(fallbackNode)
fallbackNode.firstChild.nodeValue = unicode
return calculateTextWidth(fallbackNode)
} finally {
// avoid actually rendering the test emoji
fallbackNode.remove()
}
}
return result
}

/**
* Check if the given emojis containing ZWJ characters are supported by the current browser (don't render
* as double characters) and return true if all are supported.
* @param zwjEmojisToCheck
* @param baselineEmoji
* @param baselineEmojiNode
* @param emojiToDomNode
*/
export function checkZwjSupport (zwjEmojisToCheck, baselineEmoji, emojiToDomNode) {
export function checkZwjSupport (zwjEmojisToCheck, baselineEmojiNode, emojiToDomNode) {
performance.mark('checkZwjSupport')
let allSupported = true
let shouldWarn = false
for (const emoji of zwjEmojisToCheck) {
const domNode = emojiToDomNode(emoji)
// sanity check to make sure the node is defined properly
Expand All @@ -30,14 +62,10 @@ export function checkZwjSupport (zwjEmojisToCheck, baselineEmoji, emojiToDomNode
// Just bail out of emoji support detection and return `allSupported=true` since the rendering context is gone
continue
}
const emojiWidth = calculateTextWidth(domNode)
/* istanbul ignore if */
if (emojiWidth === 0) {
shouldWarn = true
}
if (typeof baselineEmojiWidth === 'undefined') { // calculate the baseline emoji width only once
baselineEmojiWidth = calculateTextWidth(baselineEmoji)
baselineEmojiWidth = calculateTextWidthWithFallback(BASELINE_EMOJI, baselineEmojiNode, baselineEmojiNode)
}
const emojiWidth = calculateTextWidthWithFallback(emoji.unicode, domNode, baselineEmojiNode)
// On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard
// against are the ones that are 2x the size, because those are truly broken (person with red hair = person with
// floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)
Expand All @@ -55,15 +83,6 @@ export function checkZwjSupport (zwjEmojisToCheck, baselineEmoji, emojiToDomNode
console.log('Allowed borderline ZWJ emoji', emoji.unicode, emojiWidth, baselineEmojiWidth)
}
}
// Warn exactly once since we don't want to spam the console
/* istanbul ignore if */
if (shouldWarn) {
console.warn('Emoji support detection failed - emoji character is 0 width.\n' +
'This is likely due to using `display:none` which is unsupported.\n' +
'If this is a Jest/Vitest environment, you can ignore this warning.\n' +
'For details see: https://github.com/nolanlawson/emoji-picker-element/issues/514'
)
}
performance.measure('checkZwjSupport', 'checkZwjSupport')
return allSupported
}