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),
|
undefined, colors.slate),
|
||||||
pageInitialsFg: new CustomProp('theme-left-panel-page-initials-fg', undefined, 'white'),
|
pageInitialsFg: new CustomProp('theme-left-panel-page-initials-fg', undefined, 'white'),
|
||||||
pageInitialsBg: new CustomProp('theme-left-panel-page-initials-bg', undefined, colors.slate),
|
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 */
|
/* Right Panel */
|
||||||
rightPanelTabFg: new CustomProp('theme-right-panel-tab-fg', undefined, colors.dark),
|
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 { icon } from "app/client/ui2018/icons";
|
||||||
import { hoverTooltip, overflowTooltip } from 'app/client/ui/tooltips';
|
import { hoverTooltip, overflowTooltip } from 'app/client/ui/tooltips';
|
||||||
import { menu, menuItem, menuText } from "app/client/ui2018/menus";
|
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');
|
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(
|
return pageElem = dom(
|
||||||
'div',
|
'div',
|
||||||
dom.autoDispose(lis),
|
dom.autoDispose(lis),
|
||||||
|
dom.autoDispose(splitName),
|
||||||
domComputed((use) => use(name) === '', blank => blank ? dom('div', '-') :
|
domComputed((use) => use(name) === '', blank => blank ? dom('div', '-') :
|
||||||
domComputed(isRenaming, (isrenaming) => (
|
domComputed(isRenaming, (isrenaming) => (
|
||||||
isrenaming ?
|
isrenaming ?
|
||||||
cssPageItem(
|
cssPageItem(
|
||||||
cssPageInitial(
|
cssPageInitial(
|
||||||
testId('initial'),
|
testId('initial'),
|
||||||
dom.text((use) => Array.from(use(name))[0])
|
dom.text((use) => use(splitName).initial),
|
||||||
|
cssPageInitial.cls('-emoji', (use) => use(splitName).hasEmoji),
|
||||||
),
|
),
|
||||||
cssEditorInput(
|
cssEditorInput(
|
||||||
{
|
{
|
||||||
@ -82,10 +86,11 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
|
|||||||
cssPageItem(
|
cssPageItem(
|
||||||
cssPageInitial(
|
cssPageInitial(
|
||||||
testId('initial'),
|
testId('initial'),
|
||||||
dom.text((use) => Array.from(use(name))[0]),
|
dom.text((use) => use(splitName).initial),
|
||||||
|
cssPageInitial.cls('-emoji', (use) => use(splitName).hasEmoji),
|
||||||
),
|
),
|
||||||
cssPageName(
|
cssPageName(
|
||||||
dom.text(name),
|
dom.text((use) => use(splitName).displayName),
|
||||||
testId('label'),
|
testId('label'),
|
||||||
dom.on('click', (ev) => isTargetSelected(ev.target as HTMLElement) && isRenaming.set(true)),
|
dom.on('click', (ev) => isTargetSelected(ev.target as HTMLElement) && isRenaming.set(true)),
|
||||||
overflowTooltip(),
|
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', `
|
const cssPageItem = styled('a', `
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -129,7 +152,8 @@ const cssPageItem = styled('a', `
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
.${treeViewContainer.className}-close & {
|
.${treeViewContainer.className}-close & {
|
||||||
margin-left: 16px;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
&, &:hover, &:focus {
|
&, &:hover, &:focus {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -143,10 +167,25 @@ const cssPageInitial = styled('div', `
|
|||||||
color: ${theme.pageInitialsFg};
|
color: ${theme.pageInitialsFg};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-color: ${theme.pageInitialsBg};
|
background-color: ${theme.pageInitialsBg};
|
||||||
width: 16px;
|
width: 20px;
|
||||||
height: 16px;
|
height: 20px;
|
||||||
text-align: center;
|
|
||||||
margin-right: 8px;
|
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', `
|
const cssPageName = styled('div', `
|
||||||
|
@ -241,6 +241,8 @@ export interface ThemeColors {
|
|||||||
'left-panel-page-options-selected-hover-bg': string;
|
'left-panel-page-options-selected-hover-bg': string;
|
||||||
'left-panel-page-initials-fg': string;
|
'left-panel-page-initials-fg': string;
|
||||||
'left-panel-page-initials-bg': string;
|
'left-panel-page-initials-bg': string;
|
||||||
|
'left-panel-page-emoji-fg': string;
|
||||||
|
'left-panel-page-emoji-outline': string;
|
||||||
|
|
||||||
/* Right Panel */
|
/* Right Panel */
|
||||||
'right-panel-tab-fg': string;
|
'right-panel-tab-fg': string;
|
||||||
|
@ -220,6 +220,8 @@ export const GristDark: ThemeColors = {
|
|||||||
'left-panel-page-options-selected-hover-bg': '#A4A4A4',
|
'left-panel-page-options-selected-hover-bg': '#A4A4A4',
|
||||||
'left-panel-page-initials-fg': 'white',
|
'left-panel-page-initials-fg': 'white',
|
||||||
'left-panel-page-initials-bg': '#929299',
|
'left-panel-page-initials-bg': '#929299',
|
||||||
|
'left-panel-page-emoji-fg': 'black',
|
||||||
|
'left-panel-page-emoji-outline': '#69697D',
|
||||||
|
|
||||||
/* Right Panel */
|
/* Right Panel */
|
||||||
'right-panel-tab-fg': '#EFEFEF',
|
'right-panel-tab-fg': '#EFEFEF',
|
||||||
|
@ -220,6 +220,8 @@ export const GristLight: ThemeColors = {
|
|||||||
'left-panel-page-options-selected-hover-bg': '#929299',
|
'left-panel-page-options-selected-hover-bg': '#929299',
|
||||||
'left-panel-page-initials-fg': 'white',
|
'left-panel-page-initials-fg': 'white',
|
||||||
'left-panel-page-initials-bg': '#929299',
|
'left-panel-page-initials-bg': '#929299',
|
||||||
|
'left-panel-page-emoji-fg': 'white',
|
||||||
|
'left-panel-page-emoji-outline': '#BDBDBD',
|
||||||
|
|
||||||
/* Right Panel */
|
/* Right Panel */
|
||||||
'right-panel-tab-fg': '#262633',
|
'right-panel-tab-fg': '#262633',
|
||||||
|
@ -259,6 +259,42 @@ describe('Pages', function() {
|
|||||||
assert.include(await gu.getPageNames(), 'People');
|
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 () => {
|
it('should show tooltip for long page names on hover', async () => {
|
||||||
await gu.openPageMenu('People');
|
await gu.openPageMenu('People');
|
||||||
await driver.find('.test-docpage-rename').doClick();
|
await driver.find('.test-docpage-rename').doClick();
|
||||||
|
Loading…
Reference in New Issue
Block a user