Skip to content

Commit

Permalink
Replaced visibility message with indicator icon
Browse files Browse the repository at this point in the history
ref https://linear.app/ghost/issue/PLG-330/

- removed visibility text when content visibility is active
- added replacement of card type indicator icon with visibility icon when visibility settings are active
  - clicking toggles card in/out of edit mode for context-driven access to visibility settings
  • Loading branch information
kevinansfield committed Jan 30, 2025
1 parent 58489e6 commit 5d54605
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 298 deletions.
30 changes: 29 additions & 1 deletion packages/koenig-lexical/src/components/KoenigCardWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import CardContext from '../context/CardContext';
import KoenigComposerContext from '../context/KoenigComposerContext';
import React from 'react';
import {$getNodeByKey, CLICK_COMMAND, COMMAND_PRIORITY_LOW} from 'lexical';
import {CardWrapper} from './ui/CardWrapper';
import {EDIT_CARD_COMMAND, SELECT_CARD_COMMAND} from '../plugins/KoenigBehaviourPlugin';
import {DESELECT_CARD_COMMAND, EDIT_CARD_COMMAND, SELECT_CARD_COMMAND} from '../plugins/KoenigBehaviourPlugin';
import {mergeRegister} from '@lexical/utils';
import {useKoenigSelectedCardContext} from '../context/KoenigSelectedCardContext';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';

const KoenigCardWrapper = ({nodeKey, width, wrapperStyle, IndicatorIcon, children}) => {
const {cardConfig} = React.useContext(KoenigComposerContext);
const [editor] = useLexicalComposerContext();
const [cardType, setCardType] = React.useState(null);
const [captionHasFocus, setCaptionHasFocus] = React.useState(null);
Expand All @@ -20,6 +22,21 @@ const KoenigCardWrapper = ({nodeKey, width, wrapperStyle, IndicatorIcon, childre
const isSelected = selectedCardKey === nodeKey;
const isEditing = isSelected && isEditingCard;

const toggleEditMode = React.useCallback((event) => {
event.preventDefault();
event.stopPropagation();

editor.update(() => {
const cardNode = $getNodeByKey(nodeKey);

if (cardNode?.hasEditMode?.() && !isEditing) {
editor.dispatchCommand(EDIT_CARD_COMMAND, {cardKey: nodeKey, focusEditor: true});
} else if (isEditing) {
editor.dispatchCommand(DESELECT_CARD_COMMAND, {cardKey: nodeKey, focusEditor: true});
}
});
}, [editor, isEditing, nodeKey]);

React.useLayoutEffect(() => {
editor.getEditorState().read(() => {
const cardNode = $getNodeByKey(nodeKey);
Expand Down Expand Up @@ -127,6 +144,14 @@ const KoenigCardWrapper = ({nodeKey, width, wrapperStyle, IndicatorIcon, childre
};
}, [editor, isSelected, isEditing, nodeKey, containerRef]);

let isVisibilityActive = false;
if (cardConfig?.feature?.contentVisibilityAlpha) {
editor.getEditorState().read(() => {
const cardNode = $getNodeByKey(nodeKey);
isVisibilityActive = cardNode?.getIsVisibilityActive?.();
});
}

return (
<CardContext.Provider value={{
isSelected,
Expand All @@ -143,11 +168,14 @@ const KoenigCardWrapper = ({nodeKey, width, wrapperStyle, IndicatorIcon, childre
ref={containerRef}
cardType={cardType}
cardWidth={width}
feature={cardConfig?.feature}
IndicatorIcon={IndicatorIcon}
isDragging={isDragging}
isEditing={isEditing}
isSelected={isSelected}
isVisibilityActive={isVisibilityActive}
wrapperStyle={wrapperStyle}
onIndicatorClick={toggleEditMode}
>
{children}
</CardWrapper>
Expand Down
49 changes: 36 additions & 13 deletions packages/koenig-lexical/src/components/ui/CardWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import VisibilityIndicator from '../../assets/icons/kg-indicator-visibility.svg?react';

const CARD_WIDTH_CLASSES = {
wide: [
Expand All @@ -17,12 +18,14 @@ const DEFAULT_INDICATOR_POSITION = {
export const CardWrapper = React.forwardRef(({
cardType,
cardWidth,
feature,
IndicatorIcon,
indicatorPosition,
isDragging,
isEditing,
isSelected,
onClick,
isVisibilityActive,
onIndicatorClick,
wrapperStyle,
children,
...props
Expand Down Expand Up @@ -53,20 +56,40 @@ export const CardWrapper = React.forwardRef(({
...(indicatorPosition || {})
};

let indicatorIcon;
if (feature?.contentVisibilityAlpha && isVisibilityActive) {
indicatorIcon = (
<div className="sticky top-0 lg:top-8">
<VisibilityIndicator
aria-label="Card is hidden for select audiences"
className="absolute left-[-6rem] size-5 cursor-pointer text-grey"
data-testid="visibility-indicator"
style={{
left: position.left,
top: position.top
}}
onClick={onIndicatorClick}
/>
</div>
);
} else if (IndicatorIcon) {
indicatorIcon = (
<div className="sticky top-0 lg:top-8">
<IndicatorIcon
aria-label={`${cardType} indicator`}
className="absolute left-[-6rem] size-5 text-grey"
style={{
left: position.left,
top: position.top
}}
/>
</div>
);
}

return (
<>
{IndicatorIcon &&
<div className="sticky top-0 lg:top-8">
<IndicatorIcon
aria-label={`${cardType} indicator`}
className="absolute left-[-6rem] size-5 text-grey"
style={{
left: position.left,
top: position.top
}}
/>
</div>
}
{indicatorIcon}
<div
ref={ref}
className={className}
Expand Down
9 changes: 6 additions & 3 deletions packages/koenig-lexical/src/components/ui/cards/HtmlCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import {sanitizeHtml} from '../../../utils/sanitize-html';

export function HtmlCard({html, updateHtml, isEditing, darkMode, visibilityMessage}) {
const {cardConfig} = React.useContext(KoenigComposerContext);
const isContentVisibilityEnabled = cardConfig?.feature?.contentVisibility || false;
const {feature = {}} = cardConfig;
const {contentVisibility, contentVisibilityAlpha} = feature;

const displayVisibilityMessage = contentVisibility && !contentVisibilityAlpha;

return (
<>
{isEditing
? (
<>
{isContentVisibilityEnabled && <CardVisibilityMessage message={visibilityMessage} />}
{displayVisibilityMessage && <CardVisibilityMessage message={visibilityMessage} />}
<HtmlEditor
darkMode={darkMode}
html={html}
Expand All @@ -24,7 +27,7 @@ export function HtmlCard({html, updateHtml, isEditing, darkMode, visibilityMessa
</>
)
: <div>
{isContentVisibilityEnabled && <CardVisibilityMessage message={visibilityMessage} />}
{displayVisibilityMessage && <CardVisibilityMessage message={visibilityMessage} />}
<HtmlDisplay html={html} />
<div className="absolute inset-0 z-50 mt-0"></div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions packages/koenig-lexical/src/hooks/useVisibilityToggle.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {$getNodeByKey} from 'lexical';
import {generateVisibilityMessage, generateVisibilityMessageAlpha, getVisibilityOptions, parseVisibilityToToggles, serializeOptionsToVisibility, serializeTogglesToVisibility} from '../utils/visibility';
import {generateVisibilityMessage, getVisibilityOptions, parseVisibilityToToggles, serializeOptionsToVisibility, serializeTogglesToVisibility} from '../utils/visibility';

export const useVisibilityToggle = (editor, nodeKey, cardConfig) => {
const isStripeEnabled = cardConfig?.stripeEnabled;
Expand All @@ -18,8 +18,8 @@ export const useVisibilityToggle = (editor, nodeKey, cardConfig) => {
const visibilityOptions = getVisibilityOptions(currentVisibility, {isStripeEnabled});

let visibilityMessage = '';
if (isVisibilityActive) {
visibilityMessage = isContentVisibilityAlphaEnabled ? generateVisibilityMessageAlpha(currentVisibility) : generateVisibilityMessage(currentVisibility);
if (isVisibilityActive && !isContentVisibilityAlphaEnabled) {
visibilityMessage = generateVisibilityMessage(currentVisibility);
}

return {
Expand Down
1 change: 1 addition & 0 deletions packages/koenig-lexical/src/nodes/HtmlNode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class HtmlNode extends BaseHtmlNode {
return (
<KoenigCardWrapper
IndicatorIcon={HtmlIndicatorIcon}
isVisibilityActive={this.getIsVisibilityActive()}
nodeKey={this.getKey()}
wrapperStyle="wide"
>
Expand Down
82 changes: 0 additions & 82 deletions packages/koenig-lexical/src/utils/visibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,85 +135,3 @@ export function generateVisibilityMessage(visibility) {

return message;
}

export function generateVisibilityMessageAlpha(visibility) {
const toggles = parseVisibilityToToggles(visibility);
const {web, email} = toggles;

const allWeb = web.nonMembers && web.freeMembers && web.paidMembers;
const noWeb = !web.nonMembers && !web.freeMembers && !web.paidMembers;
const allWebMembers = web.freeMembers && web.paidMembers;
const noWebMembers = !web.freeMembers && !web.paidMembers;
const allEmail = email.freeMembers && email.paidMembers;
const noEmail = !email.freeMembers && !email.paidMembers;
const shownToAll = allWeb && allEmail;
const shownToNone = noWeb && noEmail;

if (shownToAll) {
return 'Visible to all web and email';
}

if (shownToNone) {
return 'Not visible on web or email';
}

let message = 'Visible to ';

if (allWeb) {
message += 'all web';

if (!noEmail) {
message += ', ';
}
} else if (!noWeb) {
if (web.nonMembers) {
message += 'anonymous';

if (!noWebMembers) {
message += ' and ';
}
}

if (allWebMembers) {
message += 'logged in';
} else {
if (web.freeMembers) {
message += 'free';

if (web.paidMembers) {
message += ', ';
}
}

if (web.paidMembers) {
message += 'paid';
}
}

message += ' web';

if (!noEmail) {
message += ', ';
}
}

if (allEmail) {
message += 'all email recipients';
} else if (!noEmail) {
if (email.freeMembers) {
message += 'free';

if (email.paidMembers) {
message += ', ';
}
}

if (email.paidMembers) {
message += 'paid';
}

message += ' email';
}

return message;
}
54 changes: 22 additions & 32 deletions packages/koenig-lexical/test/e2e/content-visibility.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ test.describe('Content Visibility', async () => {

test('toolbar shows settings panel on click', async function () {
const card = await insertHtmlCard();

await card.getByTestId('edit-html').click();

// settings are visible
Expand All @@ -185,73 +184,64 @@ test.describe('Content Visibility', async () => {

test('clicking on edit button transitions card into edit mode', async function () {
const card = await insertHtmlCard();

await card.getByTestId('edit-html').click();

await expect(card).toHaveAttribute('data-kg-card-editing', 'true');
});

test('visibility settings - defaults to show on email and web and all members', async function () {
test('visibility settings defaults to show on email and web and all members', async function () {
const card = await insertHtmlCard();

await card.getByTestId('edit-html').click();

await expect(card.getByTestId('visibility-message')).not.toBeVisible();

await expect(card.getByTestId('visibility-toggle-web-nonMembers')).toBeChecked();
await expect(card.getByTestId('visibility-toggle-web-freeMembers')).toBeChecked();
await expect(card.getByTestId('visibility-toggle-web-paidMembers')).toBeChecked();
await expect(card.getByTestId('visibility-toggle-email-freeMembers')).toBeChecked();
await expect(card.getByTestId('visibility-toggle-email-paidMembers')).toBeChecked();
});

test('can toggle visibility settings - show to anonymous web is off', async function () {
test('can toggle visibility settings ', async function () {
const card = await insertHtmlCard();

await card.getByTestId('edit-html').click();
await card.getByTestId('tab-visibility').click();
await card.getByTestId('visibility-toggle-web-nonMembers').click();

await expect(card.getByTestId('visibility-message')).toContainText('Visible to logged in web, all email recipients');
});

test('can toggle visibility settings - show to web is off', async function () {
const card = await insertHtmlCard();

await card.getByTestId('edit-html').click();
await card.getByTestId('tab-visibility').click();
await card.getByTestId('visibility-toggle-web-nonMembers').click();
await expect(card.getByTestId('visibility-toggle-web-nonMembers')).not.toBeChecked();
await card.getByTestId('visibility-toggle-web-freeMembers').click();
await expect(card.getByTestId('visibility-toggle-web-freeMembers')).not.toBeChecked();
await card.getByTestId('visibility-toggle-web-paidMembers').click();

await expect(card.getByTestId('visibility-message')).toContainText('Visible to all email recipients');
});

test('can toggle visibility settings - show on email is off', async function () {
const card = await insertHtmlCard();

await card.getByTestId('edit-html').click();
await card.getByTestId('tab-visibility').click();
await expect(card.getByTestId('visibility-toggle-web-paidMembers')).not.toBeChecked();
await card.getByTestId('visibility-toggle-email-freeMembers').click();
await expect(card.getByTestId('visibility-toggle-email-freeMembers')).not.toBeChecked();
await card.getByTestId('visibility-toggle-email-paidMembers').click();
await expect(card.getByTestId('visibility-toggle-email-paidMembers')).not.toBeChecked();

await expect(card.getByTestId('visibility-message')).toContainText('Visible to all web');
// change from the beta - visibility message is no longer shown
await expect(card.getByTestId('visibility-message')).not.toBeVisible();
});

test('can toggle visibility - disable everything', async function () {
test('visibility icon is shown when visibility changes from shown-to-all', async function () {
const card = await insertHtmlCard();

await expect(page.getByTestId('visibility-indicator')).not.toBeVisible();

await card.getByTestId('edit-html').click();
await card.getByTestId('tab-visibility').click();
await expect(card).toHaveAttribute('data-kg-card-editing', 'true');
await card.getByTestId('visibility-toggle-web-nonMembers').click();
await card.getByTestId('visibility-toggle-web-freeMembers').click();
await card.getByTestId('visibility-toggle-web-paidMembers').click();
await card.getByTestId('visibility-toggle-email-freeMembers').click();
await card.getByTestId('visibility-toggle-email-paidMembers').click();

await expect(card.getByTestId('visibility-message')).toContainText('Not visible on web or email');
await expect(page.getByTestId('visibility-indicator')).toBeVisible();

// clicking visibility indicator toggles edit mode
await page.getByTestId('visibility-indicator').click();
await expect(card).toHaveAttribute('data-kg-card-editing', 'false');
await page.getByTestId('visibility-indicator').click();
await expect(card).toHaveAttribute('data-kg-card-editing', 'true');
});

test('can toggle visibility - member settings hidden when stripe is not enabled', async function () {
test('paid member visibility settings hidden when stripe is not enabled', async function () {
await initialize({page, uri: '/#/?content=false&labs=contentVisibility,contentVisibilityAlpha&stripe=false'});
const card = await insertHtmlCard();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ describe('useVisibilityToggle', () => {
const {result} = callHook({web: {nonMember: false, memberSegment: ''}, email: {memberSegment: 'status:free,status:-free'}}, {feature: {contentVisibilityAlpha: true}});
const {visibilityMessage} = result.current;

expect(visibilityMessage).toBe('Visible to all email recipients');
// NOTE: visibility message display has been removed in contentVisibilityAlpha
expect(visibilityMessage).toBe('');
});

it('returns working toggleVisibility function', () => {
Expand Down
Loading

0 comments on commit 5d54605

Please sign in to comment.