Skip to content

Commit

Permalink
Add basic support for action attribute on button
Browse files Browse the repository at this point in the history
  • Loading branch information
lemonmade committed Sep 22, 2024
1 parent 0bd545c commit 8e51c67
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 57 deletions.
6 changes: 6 additions & 0 deletions .changeset/perfect-pets-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@watching/design': patch
'@watching/clips': patch
---

Add basic support for `action` attribute on button
101 changes: 52 additions & 49 deletions app/shared/clips/Clip/Clip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {ThreadRendererInstance} from '@watching/thread-render';

import {useGraphQLMutation} from '~/shared/graphql.ts';

import {ClipsExtensionPointBeingRenderedContext} from '../context.ts';
import {useClipsManager} from '../react.tsx';
import {
type ClipsExtensionPoint,
Expand Down Expand Up @@ -50,56 +51,58 @@ export function Clip<Point extends ExtensionPoint>({
const renderer = local ?? installed;

return (
<Section>
<BlockStack spacing>
<ContentAction
overlay={
<Popover inlineAttachment="start">
{installed?.instance.value && (
<Section padding>
<ClipSettings
id={extension.id}
instance={installed.instance.value}
/>
</Section>
)}
<Menu>
<ViewAppAction />
{renderer && <RestartClipButton instance={renderer} />}
{extension.installed && (
<UninstallClipButton extension={extension} />
<ClipsExtensionPointBeingRenderedContext.Provider value={extension}>
<Section>
<BlockStack spacing>
<ContentAction
overlay={
<Popover inlineAttachment="start">
{installed?.instance.value && (
<Section padding>
<ClipSettings
id={extension.id}
instance={installed.instance.value}
/>
</Section>
)}
{extension.installed && <ReportIssueButton />}
</Menu>
</Popover>
}
>
<InlineGrid sizes={['auto', 'fill']} spacing="small">
<View
display="inlineFlex"
background="emphasized"
border="subdued"
cornerRadius
alignment="center"
blockSize={Style.css`2.5rem`}
inlineSize={Style.css`2.5rem`}
>
<Icon source="app" />
</View>
<BlockStack>
<Text emphasis accessibilityRole="heading">
{name}
</Text>
<Text emphasis="subdued" size="small">
from app <Text emphasis>{app.name}</Text>
</Text>
</BlockStack>
</InlineGrid>
</ContentAction>

{renderer && <ClipInstanceRenderer renderer={renderer} />}
</BlockStack>
</Section>
<Menu>
<ViewAppAction />
{renderer && <RestartClipButton instance={renderer} />}
{extension.installed && (
<UninstallClipButton extension={extension} />
)}
{extension.installed && <ReportIssueButton />}
</Menu>
</Popover>
}
>
<InlineGrid sizes={['auto', 'fill']} spacing="small">
<View
display="inlineFlex"
background="emphasized"
border="subdued"
cornerRadius
alignment="center"
blockSize={Style.css`2.5rem`}
inlineSize={Style.css`2.5rem`}
>
<Icon source="app" />
</View>
<BlockStack>
<Text emphasis accessibilityRole="heading">
{name}
</Text>
<Text emphasis="subdued" size="small">
from app <Text emphasis>{app.name}</Text>
</Text>
</BlockStack>
</InlineGrid>
</ContentAction>

{renderer && <ClipInstanceRenderer renderer={renderer} />}
</BlockStack>
</Section>
</ClipsExtensionPointBeingRenderedContext.Provider>
);
}

Expand Down
59 changes: 54 additions & 5 deletions app/shared/clips/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {Button as UIButton} from '@lemon/zest';
import {Button as UIButton, type ButtonProps} from '@lemon/zest';

import {useRenderClipsExtensionPointBeingRendered} from '../context.ts';
import type {ClipsExtensionPoint} from '../extension.ts';

import {
createClipsComponentRenderer,
useRenderedChildren,
Expand All @@ -8,24 +12,69 @@ import {
export const Button = createClipsComponentRenderer(
'ui-button',
function Button(props) {
const extension = useRenderClipsExtensionPointBeingRendered();

const {overlay, children} = useRenderedChildren(props, {
slotProps: ['overlay'],
});

const attributes = props.element.attributes.value;
const events = props.element.eventListeners.value;

const action = parseAction(attributes.action);

let to: string | undefined = attributes.to;
let onPress: ButtonProps['onPress'] | undefined = undefined;

switch (action.type) {
case 'auto': {
onPress = events.press
? wrapEventListenerForCallback(events.press)
: undefined;
break;
}
case 'navigate': {
to = action.url;
break;
}
case 'mutate': {
onPress = createMutateAction(action.mutation, extension);
break;
}
}

return (
<UIButton
to={attributes.to}
to={to}
disabled={attributes.disabled != null}
onPress={
events.press ? wrapEventListenerForCallback(events.press) : undefined
}
onPress={onPress}
overlay={overlay}
>
{children}
</UIButton>
);
},
);

function parseAction(action: string | undefined) {
if (action == null) return {type: 'auto' as const};

if (action.startsWith('navigate(') && action.endsWith(')')) {
return {type: 'navigate' as const, url: action.slice(8, -1)};
}

if (action.startsWith('mutate(') && action.endsWith(')')) {
return {type: 'mutate' as const, mutation: action.slice(7, -1)};
}

return {type: 'auto' as const};
}

function createMutateAction(
mutation: string,
extensionPoint: ClipsExtensionPoint<any>,
) {
return async function mutate() {
console.log(`TODO: run mutation "${mutation}"`, extensionPoint);
};
}
8 changes: 8 additions & 0 deletions app/shared/clips/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {createOptionalContext} from '@quilted/quilt/context';
import type {ClipsExtensionPoint} from './extension.ts';

export const ClipsExtensionPointBeingRenderedContext =
createOptionalContext<ClipsExtensionPoint<any>>();

export const useRenderClipsExtensionPointBeingRendered =
ClipsExtensionPointBeingRenderedContext.use;
2 changes: 2 additions & 0 deletions app/shared/clips/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {type Version, type ExtensionPoint, type Api} from '@watching/clips';
import {type OptionsForExtensionPoint} from './extension-points.ts';
import {type Sandbox} from './sandbox.ts';
import {type LiveQueryRunner} from './live-query.ts';
import {type ClipsManager} from './manager.ts';

export type {Version, ExtensionPoint};

export interface ClipsExtensionPoint<Point extends ExtensionPoint> {
readonly id: string;
readonly target: Point;
readonly extension: ClipsExtension;
readonly manager: ClipsManager;
readonly local?: ClipsExtensionPointLocalInstanceOptions<Point>;
readonly installed?: ClipsExtensionPointInstalledInstanceOptions<Point>;
}
Expand Down
8 changes: 6 additions & 2 deletions app/shared/clips/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export function useClips<Point extends ExtensionPoint>(
: [never?]
): readonly ClipsExtensionPoint<Point>[] {
const [options] = optionsArg;

const manager = useClipsManager();
const server = manager.localDevelopment;

const installedClips = useMemo(() => {
if (installations == null) return [];

Expand All @@ -63,6 +67,7 @@ export function useClips<Point extends ExtensionPoint>(
installedClips.push({
id: `${id}:${point}`,
target: point,
manager,
extension: {
id: extension.id,
name: extension.name,
Expand All @@ -89,8 +94,6 @@ export function useClips<Point extends ExtensionPoint>(
return installedClips;
}, [installations, point, options]);

const server = useClipsManager().localDevelopment;

const allClips = useComputed(() => {
const allLocalClips = server.extensions.value;

Expand All @@ -108,6 +111,7 @@ export function useClips<Point extends ExtensionPoint>(
localClips.push({
id: `${id}:${point}`,
target: point,
manager,
extension: {
id,
name,
Expand Down
11 changes: 10 additions & 1 deletion packages/clips/source/elements/Button/Button.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {ButtonAction} from '@watching/design';
import type {
RemoteEvent,
RemoteElementEventListenersDefinition,
Expand All @@ -19,6 +20,11 @@ export interface ButtonAttributes {
* Disallows interaction with the button.
*/
disabled?: '';

/**
* An action to perform when the button is pressed.
*/
action?: ButtonAction;
}

export interface ButtonProperties {
Expand Down Expand Up @@ -56,14 +62,17 @@ export class Button
}

static get remoteAttributes() {
return ['to', 'disabled'] satisfies (keyof ButtonAttributes)[];
return ['action', 'to', 'disabled'] satisfies (keyof ButtonAttributes)[];
}

@backedByAttribute()
accessor to: string | undefined;

@backedByAttributeAsBoolean()
accessor disabled: boolean = false;

@backedByAttribute()
accessor action: ButtonAction | undefined;
}

customElements.define('ui-button', Button);
Expand Down
6 changes: 6 additions & 0 deletions packages/design/source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ export const IMAGE_ACCESSIBILITY_ROLE_KEYWORDS =

export type ViewportResolution = 1 | 1.3 | 1.5 | 2 | 2.6 | 3 | 3.5 | 4;

// Button

export type ButtonNavigateAction = `navigate(${string})`;
export type ButtonMutateAction = `mutate(${string})`;
export type ButtonAction = 'auto' | ButtonNavigateAction | ButtonMutateAction;

// Popover

export type PopoverAttachmentKeyword = 'auto' | 'start' | 'center' | 'end';
Expand Down

0 comments on commit 8e51c67

Please sign in to comment.