Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/react-components-bookmark_app-loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@equinor/fusion-framework-react-components-bookmark": patch
---

Show accessible loading feedback for the app name while the edit bookmark modal resolves its app manifest.
2 changes: 1 addition & 1 deletion packages/react/components/bookmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The provider manages modal state for creating, editing, and importing bookmarks.
- **Filtering** — text search across bookmark names
- **Grouping** — group bookmarks by app, creator, or other criteria
- **Create modal** — form for creating new bookmarks
- **Edit modal** — form for updating existing bookmarks
- **Edit modal** — form for updating existing bookmarks with loading feedback while resolving the source app name
- **Import modal** — import bookmarks shared via URL
- **Clipboard sharing** — copy bookmark URL to clipboard

Expand Down
6 changes: 4 additions & 2 deletions packages/react/components/bookmark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"types": "./dist/types/index.d.ts",
"scripts": {
"build": "tsc -b",
"prepack": "pnpm build"
"prepack": "pnpm build",
"test": "vitest --run"
},
"keywords": [],
"author": "",
Expand Down Expand Up @@ -42,7 +43,8 @@
"@types/react": "^19.2.7",
"react": "^19.2.1",
"styled-components": "^6.3.11",
"typescript": "^7.0.2"
"typescript": "^7.0.2",
"vitest": "^4.1.0"
},
"peerDependencies": {
"@equinor/eds-core-react": "^2.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { isValidElement } from 'react';
import { Input, Progress } from '@equinor/eds-core-react';
import { describe, expect, it } from 'vitest';
import { AppNameField } from '../components/edit-bookmark/AppNameField';

describe('AppNameField', () => {
it('shows an accessible progress indicator while the manifest is loading', () => {
const field = AppNameField({ isLoading: true });
const loadingIndicator = field.props.rightAdornments;

expect(field.type).toBe(Input);
expect(field.props['aria-busy']).toBe(true);
expect(isValidElement(loadingIndicator)).toBe(true);

// Guard the element shape before inspecting progress-specific props.
if (!isValidElement(loadingIndicator)) {
throw new Error('Expected a valid loading indicator');
}

expect(loadingIndicator.type).toBe(Progress.Circular);
expect(loadingIndicator.props).toMatchObject({
'aria-label': 'Loading app name',
size: 16,
});
});

it('shows the resolved display name without a progress indicator', () => {
const field = AppNameField({
displayName: 'My app',
isLoading: false,
});

expect(field.props.value).toBe('My app');
expect(field.props['aria-busy']).toBe(false);
expect(field.props.rightAdornments).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Input, Progress } from '@equinor/eds-core-react';

/**
* Renders the bookmark's app name while making manifest resolution visible.
*
* @param props - The resolved display name and whether the manifest request is pending.
* @returns A read-only app name field with an accessible loading indicator when needed.
*/
export const AppNameField = ({
displayName,
isLoading,
}: {
readonly displayName?: string;
readonly isLoading: boolean;
}) => (
<Input
id="app"
readOnly={true}
value={displayName ?? ''}
aria-busy={isLoading}
rightAdornments={
isLoading ? <Progress.Circular aria-label="Loading app name" size={16} /> : undefined
}
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Button, Checkbox, Dialog, Input, Label, Textarea } from '@equinor/eds-c
import styled from 'styled-components';

import { useBookmarkComponentContext } from '../BookmarkProvider';
import { AppNameField } from './AppNameField';

const Styled = {
Dialog: styled(Dialog)`
Expand Down Expand Up @@ -81,12 +82,16 @@ export const EditBookmarkModal = ({

// TODO(#5091): this should be on the bookmark object
const appProvider = useFrameworkModule<AppModule>('app');
const { value: appName } = useObservableState(
const { value: appName, error: appNameError } = useObservableState(
useMemo(
() => (bookmark && appProvider ? appProvider.getAppManifest(bookmark.appKey) : of(undefined)),
[appProvider, bookmark],
),
);
// Only report a pending manifest request; missing providers and failed requests are not loading.
const isAppNameLoading = Boolean(
bookmark && appProvider && appName === undefined && appNameError === null,
);

const updateBookmark = useCallback(
async (updates: BookmarkUpdate) => {
Expand Down Expand Up @@ -146,8 +151,7 @@ export const EditBookmarkModal = ({
</div>
<div>
<Label htmlFor="app" label="App" />
{/** TODO(#5092): show ghost while loading app name */}
<Input readOnly={true} value={appName?.displayName || ''} />
<AppNameField displayName={appName?.displayName} isLoading={isAppNameLoading} />
</div>
Comment thread
odinr marked this conversation as resolved.

<Styled.CheckboxWrapper>
Expand Down
2 changes: 1 addition & 1 deletion packages/react/components/bookmark/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
}
],
"include": ["src/**/*"],
"exclude": ["node_modules", "lib"]
"exclude": ["node_modules", "lib", "src/__tests__/**/*"]
}
10 changes: 10 additions & 0 deletions packages/react/components/bookmark/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineProject } from 'vitest/config';

import { name, version } from './package.json';

export default defineProject({
test: {
include: ['src/__tests__/**/*.{test,spec}.{ts,tsx}'],
name: `${name}@${version}`,
},
});
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading