-
Notifications
You must be signed in to change notification settings - Fork 911
Fleet UI: Unreleased bug fixes to command palette (AB vs ABM, Controls on Fleet Free, emoji not italicized) #47432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
e061512
FE: Rename command palette ABM labels to Apple Business (AB) (#47384)
RachelElysia 98ae0bb
FE: Surface Controls in command palette on Fleet Free
RachelElysia 58ee4a5
FE: Keep emoji upright in italicized command palette text
RachelElysia 6a65309
FE: Hide command palette entries that land on the Fleet Premium upsell
RachelElysia b27392e
Potential fix for pull request finding
RachelElysia 38cc8ed
Potential fix for pull request finding
RachelElysia 2289492
FE: Test modifier + ZWJ emoji sequence in UprightEmoji
RachelElysia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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
109 changes: 109 additions & 0 deletions
109
frontend/components/CommandPalette/components/UprightEmoji.tests.tsx
This file contains hidden or 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,109 @@ | ||
| import React from "react"; | ||
| import { render } from "@testing-library/react"; | ||
|
|
||
| import UprightEmoji, { splitEmojiSegments } from "./UprightEmoji"; | ||
|
|
||
| describe("splitEmojiSegments", () => { | ||
| it("returns a single non-emoji segment for an emoji-free string", () => { | ||
| expect(splitEmojiSegments("Workstations")).toEqual([ | ||
| { text: "Workstations", isEmoji: false }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("returns an empty segment for an empty string", () => { | ||
| expect(splitEmojiSegments("")).toEqual([{ text: "", isEmoji: false }]); | ||
| }); | ||
|
|
||
| it("splits an emoji prefix from the trailing text", () => { | ||
| expect(splitEmojiSegments("💻 Workstations")).toEqual([ | ||
| { text: "💻", isEmoji: true }, | ||
| { text: " Workstations", isEmoji: false }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("coalesces adjacent emoji into one segment", () => { | ||
| expect(splitEmojiSegments("📱🔐 Personal mobile devices")).toEqual([ | ||
| { text: "📱🔐", isEmoji: true }, | ||
| { text: " Personal mobile devices", isEmoji: false }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("handles emoji inside the string (not just at the start)", () => { | ||
| expect(splitEmojiSegments("Team 🚀 alpha")).toEqual([ | ||
| { text: "Team ", isEmoji: false }, | ||
| { text: "🚀", isEmoji: true }, | ||
| { text: " alpha", isEmoji: false }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("handles a ZWJ-joined emoji as a single segment", () => { | ||
| // Man technologist: 👨 + ZWJ + 💻 → one grapheme | ||
| expect(splitEmojiSegments("👨💻 Engineering")).toEqual([ | ||
| { text: "👨💻", isEmoji: true }, | ||
| { text: " Engineering", isEmoji: false }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("handles emoji modifiers (skin tone) without splitting the grapheme", () => { | ||
| expect(splitEmojiSegments("👍🏽 Team")).toEqual([ | ||
| { text: "👍🏽", isEmoji: true }, | ||
| { text: " Team", isEmoji: false }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("handles modifier + ZWJ sequences as a single segment", () => { | ||
| // Man technologist with medium skin tone: 👨 + 🏽 + ZWJ + 💻 | ||
| expect(splitEmojiSegments("👨🏽💻 Engineering")).toEqual([ | ||
| { text: "👨🏽💻", isEmoji: true }, | ||
| { text: " Engineering", isEmoji: false }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("handles a regional indicator pair (flag emoji) as one segment", () => { | ||
| // 🇺🇸 = U+1F1FA U+1F1F8 | ||
| expect(splitEmojiSegments("\u{1F1FA}\u{1F1F8} US fleet")).toEqual([ | ||
| { text: "\u{1F1FA}\u{1F1F8}", isEmoji: true }, | ||
| { text: " US fleet", isEmoji: false }, | ||
| ]); | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| it("handles emoji at the very end of the string", () => { | ||
| expect(splitEmojiSegments("Sales team 🏆")).toEqual([ | ||
| { text: "Sales team ", isEmoji: false }, | ||
| { text: "🏆", isEmoji: true }, | ||
| ]); | ||
| }); | ||
| }); | ||
|
|
||
| describe("UprightEmoji", () => { | ||
| it("renders plain text when there are no emoji", () => { | ||
| const { container } = render(<UprightEmoji text="Workstations" />); | ||
| expect(container.textContent).toBe("Workstations"); | ||
| expect(container.querySelectorAll("span")).toHaveLength(0); | ||
| }); | ||
|
|
||
| it("wraps emoji segments in a span with font-style: normal", () => { | ||
| const { container } = render(<UprightEmoji text="💻 Workstations" />); | ||
| const spans = container.querySelectorAll("span"); | ||
| expect(spans).toHaveLength(1); | ||
| expect(spans[0].textContent).toBe("💻"); | ||
| expect(spans[0].style.fontStyle).toBe("normal"); | ||
| expect(container.textContent).toBe("💻 Workstations"); | ||
| }); | ||
|
|
||
| it("coalesces adjacent emoji into a single span", () => { | ||
| const { container } = render( | ||
| <UprightEmoji text="📱🔐 Personal mobile devices" /> | ||
| ); | ||
| const spans = container.querySelectorAll("span"); | ||
| expect(spans).toHaveLength(1); | ||
| expect(spans[0].textContent).toBe("📱🔐"); | ||
| }); | ||
|
|
||
| it("preserves a ZWJ sequence as one upright span", () => { | ||
| const { container } = render(<UprightEmoji text="👨💻 Engineering" />); | ||
| const spans = container.querySelectorAll("span"); | ||
| expect(spans).toHaveLength(1); | ||
| expect(spans[0].textContent).toBe("👨💻"); | ||
| }); | ||
| }); | ||
109 changes: 109 additions & 0 deletions
109
frontend/components/CommandPalette/components/UprightEmoji.tsx
This file contains hidden or 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,109 @@ | ||
| import React from "react"; | ||
|
|
||
| // Matches an emoji "grapheme" — a single emoji codepoint with optional | ||
| // variation selector / ZWJ continuations, OR a regional indicator pair | ||
| // (used for flag emoji like 🇺🇸). Browsers fake italic by shearing every | ||
| // glyph including emoji, so wrapping emoji in `font-style: normal` opts | ||
| // them out of the synthetic slant while leaving Latin text italic via | ||
| // the parent's `font-style: italic`. | ||
| // \uFE0F = variation selector-16 (force emoji presentation) | ||
| // \u200D = zero-width joiner (combines emoji into one grapheme, | ||
| // e.g. 👨💻 → "man technologist") | ||
| const EMOJI_RUN_RE = /(?:\p{Extended_Pictographic}(?:\p{Emoji_Modifier})?\uFE0F?(?:\u200D\p{Extended_Pictographic}(?:\p{Emoji_Modifier})?\uFE0F?)*|\p{Regional_Indicator}\p{Regional_Indicator})/gu; | ||
|
|
||
| export interface IEmojiSegment { | ||
| text: string; | ||
| isEmoji: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Split `text` into alternating emoji vs non-emoji segments. Pure helper | ||
| * exposed for testing; production callers use the <UprightEmoji /> wrapper. | ||
| * | ||
| * Returns a single non-emoji segment when no emoji are present, so callers | ||
| * can fast-path the bare-string case without diffing array shapes. | ||
| */ | ||
| export const splitEmojiSegments = (text: string): IEmojiSegment[] => { | ||
| if (!text) return [{ text: "", isEmoji: false }]; | ||
| // Collect match positions via String.replace with a callback. We don't | ||
| // actually replace anything — replace just exposes (match, offset) for | ||
| // each hit. Using replace instead of String.matchAll keeps the helper | ||
| // compatible with pre-ES2020 lib targets and sidesteps mutating the | ||
| // global regex's lastIndex. | ||
| const matches: Array<{ start: number; end: number }> = []; | ||
| text.replace(EMOJI_RUN_RE, (match, offset) => { | ||
| const start = offset as number; | ||
| matches.push({ start, end: start + match.length }); | ||
| return match; | ||
| }); | ||
|
|
||
| const segments: IEmojiSegment[] = []; | ||
| let cursor = 0; | ||
| // Coalesce adjacent emoji matches (e.g. "📱🔐") into one segment so the | ||
| // DOM stays compact and the span attribute count doesn't balloon for | ||
| // rows that prefix two or three emoji. | ||
| let pendingEmojiStart = -1; | ||
| let pendingEmojiEnd = -1; | ||
| const flushEmoji = () => { | ||
| if (pendingEmojiStart === -1) return; | ||
| segments.push({ | ||
| text: text.slice(pendingEmojiStart, pendingEmojiEnd), | ||
| isEmoji: true, | ||
| }); | ||
| pendingEmojiStart = -1; | ||
| pendingEmojiEnd = -1; | ||
| }; | ||
| matches.forEach(({ start, end }) => { | ||
| if (start === pendingEmojiEnd) { | ||
| pendingEmojiEnd = end; | ||
| cursor = end; | ||
| return; | ||
| } | ||
| flushEmoji(); | ||
| if (start > cursor) { | ||
| segments.push({ text: text.slice(cursor, start), isEmoji: false }); | ||
| } | ||
| pendingEmojiStart = start; | ||
| pendingEmojiEnd = end; | ||
| cursor = end; | ||
| }); | ||
| flushEmoji(); | ||
| if (cursor < text.length) { | ||
| segments.push({ text: text.slice(cursor), isEmoji: false }); | ||
| } | ||
| if (segments.length === 0) return [{ text, isEmoji: false }]; | ||
| return segments; | ||
| }; | ||
|
|
||
| interface IUprightEmojiProps { | ||
| text: string; | ||
| } | ||
|
|
||
| /** | ||
| * Renders `text` so emoji glyphs stay upright even when the surrounding | ||
| * element has `font-style: italic`. Use anywhere fleet names (or other | ||
| * user-provided strings that may carry emoji) appear inside italicized | ||
| * surfaces — synthetic italic shears emoji and looks broken. | ||
| */ | ||
| const UprightEmoji = ({ text }: IUprightEmojiProps): JSX.Element => { | ||
| const segments = splitEmojiSegments(text); | ||
| return ( | ||
| <> | ||
| {segments.map((seg, i) => | ||
| seg.isEmoji ? ( | ||
| // Index keys are safe — segments derive synchronously from the | ||
| // same input each render, so order is stable. | ||
| // eslint-disable-next-line react/no-array-index-key | ||
| <span key={i} style={{ fontStyle: "normal" }}> | ||
| {seg.text} | ||
| </span> | ||
| ) : ( | ||
| // eslint-disable-next-line react/no-array-index-key | ||
| <React.Fragment key={i}>{seg.text}</React.Fragment> | ||
| ) | ||
| )} | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default UprightEmoji; |
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.