mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Detect when a page initial is an emoji, avoid repeating it, and style it better
Summary: - Detecting emoji is surprisingly tricky; we use a fancy regex as a decent heuristic. - Icons are a little larger than before. - Styling tweaked for light and dark modes - In case the OS doesn't render the emoji as one character, truncate what's shown in the icon box. Test Plan: Added a test case. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3904
This commit is contained in:
parent
4ea748b1a3
commit
b0aa17c932
@ -426,6 +426,9 @@ export const theme = {
|
||||
undefined, colors.slate),
|
||||
pageInitialsFg: new CustomProp('theme-left-panel-page-initials-fg', undefined, 'white'),
|
||||
pageInitialsBg: new CustomProp('theme-left-panel-page-initials-bg', undefined, colors.slate),
|
||||
pageInitialsEmojiBg: new CustomProp('theme-left-panel-page-emoji-fg', undefined, 'white'),
|
||||
pageInitialsEmojiOutline: new CustomProp('theme-left-panel-page-emoji-outline', undefined,
|
||||
colors.darkGrey),
|
||||
|
||||
/* Right Panel */
|
||||
rightPanelTabFg: new CustomProp('theme-right-panel-tab-fg', undefined, colors.dark),
|
||||
|
@ -6,7 +6,7 @@ import { theme } from "app/client/ui2018/cssVars";
|
||||
import { icon } from "app/client/ui2018/icons";
|
||||
import { hoverTooltip, overflowTooltip } from 'app/client/ui/tooltips';
|
||||
import { menu, menuItem, menuText } from "app/client/ui2018/menus";
|
||||
import { dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs";
|
||||
import { Computed, dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs";
|
||||
|
||||
const t = makeT('pages');
|
||||
|
||||
@ -54,16 +54,20 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
|
||||
}
|
||||
});
|
||||
|
||||
const splitName = Computed.create(null, name, (use, _name) => splitPageInitial(_name));
|
||||
|
||||
return pageElem = dom(
|
||||
'div',
|
||||
dom.autoDispose(lis),
|
||||
dom.autoDispose(splitName),
|
||||
domComputed((use) => use(name) === '', blank => blank ? dom('div', '-') :
|
||||
domComputed(isRenaming, (isrenaming) => (
|
||||
isrenaming ?
|
||||
cssPageItem(
|
||||
cssPageInitial(
|
||||
testId('initial'),
|
||||
dom.text((use) => Array.from(use(name))[0])
|
||||
dom.text((use) => use(splitName).initial),
|
||||
cssPageInitial.cls('-emoji', (use) => use(splitName).hasEmoji),
|
||||
),
|
||||
cssEditorInput(
|
||||
{
|
||||
@ -82,10 +86,11 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
|
||||
cssPageItem(
|
||||
cssPageInitial(
|
||||
testId('initial'),
|
||||
dom.text((use) => Array.from(use(name))[0]),
|
||||
dom.text((use) => use(splitName).initial),
|
||||
cssPageInitial.cls('-emoji', (use) => use(splitName).hasEmoji),
|
||||
),
|
||||
cssPageName(
|
||||
dom.text(name),
|
||||
dom.text((use) => use(splitName).displayName),
|
||||
testId('label'),
|
||||
dom.on('click', (ev) => isTargetSelected(ev.target as HTMLElement) && isRenaming.set(true)),
|
||||
overflowTooltip(),
|
||||
@ -122,6 +127,24 @@ export function buildCensoredPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// This crazy expression matches all "possible emoji" and comes from a very official source:
|
||||
// https://unicode.org/reports/tr51/#EBNF_and_Regex (linked from
|
||||
// https://stackoverflow.com/a/68146409/328565). It is processed from the original by replacing \x
|
||||
// with \u, removing whitespace, and factoring out a long subexpression.
|
||||
const emojiPart = /(?:\p{RI}\p{RI}|\p{Emoji}(?:\p{EMod}|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?)/u;
|
||||
const pageInitialRegex = new RegExp(`^${emojiPart.source}(?:\\u{200D}${emojiPart.source})*`, "u");
|
||||
|
||||
// Divide up the page name into an "initial" and "displayName", where an emoji initial, if
|
||||
// present, is omitted from the displayName, but a regular character used as the initial is kept.
|
||||
function splitPageInitial(name: string): {initial: string, displayName: string, hasEmoji: boolean} {
|
||||
const m = name.match(pageInitialRegex);
|
||||
if (m) {
|
||||
return {initial: m[0], displayName: name.slice(m[0].length).trim(), hasEmoji: true};
|
||||
} else {
|
||||
return {initial: Array.from(name)[0], displayName: name.trim(), hasEmoji: false};
|
||||
}
|
||||
}
|
||||
|
||||
const cssPageItem = styled('a', `
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -129,7 +152,8 @@ const cssPageItem = styled('a', `
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
.${treeViewContainer.className}-close & {
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
&, &:hover, &:focus {
|
||||
text-decoration: none;
|
||||
@ -143,10 +167,25 @@ const cssPageInitial = styled('div', `
|
||||
color: ${theme.pageInitialsFg};
|
||||
border-radius: 3px;
|
||||
background-color: ${theme.pageInitialsBg};
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-emoji {
|
||||
background-color: ${theme.pageInitialsEmojiBg};
|
||||
box-shadow: 0 0 0 1px var(--grist-theme-left-panel-page-emoji-outline, var(--grist-color-dark-grey));
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.${treeViewContainer.className}-close & {
|
||||
margin-right: 0;
|
||||
}
|
||||
.${itemHeader.className}.selected &-emoji {
|
||||
box-shadow: none;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssPageName = styled('div', `
|
||||
|
@ -241,6 +241,8 @@ export interface ThemeColors {
|
||||
'left-panel-page-options-selected-hover-bg': string;
|
||||
'left-panel-page-initials-fg': string;
|
||||
'left-panel-page-initials-bg': string;
|
||||
'left-panel-page-emoji-fg': string;
|
||||
'left-panel-page-emoji-outline': string;
|
||||
|
||||
/* Right Panel */
|
||||
'right-panel-tab-fg': string;
|
||||
|
@ -220,6 +220,8 @@ export const GristDark: ThemeColors = {
|
||||
'left-panel-page-options-selected-hover-bg': '#A4A4A4',
|
||||
'left-panel-page-initials-fg': 'white',
|
||||
'left-panel-page-initials-bg': '#929299',
|
||||
'left-panel-page-emoji-fg': 'black',
|
||||
'left-panel-page-emoji-outline': '#69697D',
|
||||
|
||||
/* Right Panel */
|
||||
'right-panel-tab-fg': '#EFEFEF',
|
||||
|
@ -220,6 +220,8 @@ export const GristLight: ThemeColors = {
|
||||
'left-panel-page-options-selected-hover-bg': '#929299',
|
||||
'left-panel-page-initials-fg': 'white',
|
||||
'left-panel-page-initials-bg': '#929299',
|
||||
'left-panel-page-emoji-fg': 'white',
|
||||
'left-panel-page-emoji-outline': '#BDBDBD',
|
||||
|
||||
/* Right Panel */
|
||||
'right-panel-tab-fg': '#262633',
|
||||
|
@ -259,6 +259,42 @@ describe('Pages', function() {
|
||||
assert.include(await gu.getPageNames(), 'People');
|
||||
});
|
||||
|
||||
it('should pull out emoji from page names', async () => {
|
||||
// A regular character is used as an initial AND kept in the name.
|
||||
assert.deepEqual(await getInitialAndName(/People/), ['P', 'People']);
|
||||
|
||||
// It looks like our version of Chromedriver does not support sending emojis using sendKeys
|
||||
// (issue mentioned here https://stackoverflow.com/a/59139690), so we'll use executeScript to
|
||||
// rename pages.
|
||||
async function renamePage(origName: string, newName: string) {
|
||||
await gu.openPageMenu(origName);
|
||||
await driver.find('.test-docpage-rename').doClick();
|
||||
const editor = await driver.find('.test-docpage-editor');
|
||||
await driver.executeScript((el: HTMLInputElement, text: string) => { el.value = text; }, editor, newName);
|
||||
await editor.sendKeys(Key.ENTER);
|
||||
await gu.waitForServer();
|
||||
}
|
||||
|
||||
async function getInitialAndName(pageName: string|RegExp): Promise<[string, string]> {
|
||||
return await driver.findContent('.test-treeview-itemHeader', pageName)
|
||||
.findAll('.test-docpage-initial, .test-docpage-label', el => el.getText()) as [string,
|
||||
string];
|
||||
}
|
||||
|
||||
// An emoji is pulled into the initial, and is removed from the name.
|
||||
await renamePage('People', '👥 People');
|
||||
|
||||
assert.deepEqual(await getInitialAndName(/People/), ['👥', 'People']);
|
||||
|
||||
// Two complex emojis -- the first one is the pulled-out initial.
|
||||
await renamePage('People', '👨👩👧👦👨👩👧Guest List');
|
||||
assert.deepEqual(await getInitialAndName(/Guest List/),
|
||||
['👨👩👧👦', '👨👩👧Guest List']);
|
||||
|
||||
await gu.undo(2);
|
||||
assert.deepEqual(await getInitialAndName(/People/), ['P', 'People']);
|
||||
});
|
||||
|
||||
it('should show tooltip for long page names on hover', async () => {
|
||||
await gu.openPageMenu('People');
|
||||
await driver.find('.test-docpage-rename').doClick();
|
||||
|
Loading…
Reference in New Issue
Block a user