Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
68 changes: 13 additions & 55 deletions packages/main/src/components/ObjectPage/ObjectPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const Default: Story = {
return (
// `onBeforeNavigate` & `onToggleHeaderArea` have to be added like this, otherwise Storybook invokes them incorrectly sometimes
<ObjectPage {...args} onBeforeNavigate={args.onBeforeNavigate} onToggleHeaderArea={args.onToggleHeaderArea}>
<ObjectPageSection titleText="Goals" id="goals" aria-label="Goals">
<ObjectPageSection titleText="Goals" id="goals">
<Form layout="S1 M2 L3 XL3" labelSpan="S12 M12 L12 XL12">
<FormItem labelContent={<Label showColon>Evangelize the UI framework across the company</Label>}>
<Text>4 days overdue - Cascaded</Text>
Expand All @@ -180,11 +180,10 @@ export const Default: Story = {
</FormItem>
</Form>
</ObjectPageSection>
<ObjectPageSection titleText="Personal" id="personal" aria-label="Personal">
<ObjectPageSection titleText="Personal" id="personal">
<ObjectPageSubSection
titleText="Connect"
id="personal-connect"
aria-label="Connect"
actions={
<>
<Button design={ButtonDesign.Emphasized} style={{ minWidth: '120px' }}>
Expand Down Expand Up @@ -225,11 +224,7 @@ export const Default: Story = {
</FormGroup>
</Form>
</ObjectPageSubSection>
<ObjectPageSubSection
titleText="Payment Information"
id="personal-payment-information"
aria-label="Payment Information"
>
<ObjectPageSubSection titleText="Payment Information" id="personal-payment-information">
<Form>
<FormGroup headerText="Salary">
<FormItem labelContent={<Label showColon>Bank Transfer</Label>}>
Expand All @@ -244,12 +239,8 @@ export const Default: Story = {
</Form>
</ObjectPageSubSection>
</ObjectPageSection>
<ObjectPageSection titleText="Employment" id="employment" aria-label="Employment">
<ObjectPageSubSection
titleText="Job Information"
id="employment-job-information"
aria-label="Job Information"
>
<ObjectPageSection titleText="Employment" id="employment">
<ObjectPageSubSection titleText="Job Information" id="employment-job-information">
<Form>
<FormItem labelContent={<Label showColon>Job Classification</Label>}>
<FlexBox direction={FlexBoxDirection.Column}>
Expand Down Expand Up @@ -277,11 +268,7 @@ export const Default: Story = {
</FormItem>
</Form>
</ObjectPageSubSection>
<ObjectPageSubSection
titleText="Employee Details"
id="employment-employee-details"
aria-label="Employee Details"
>
<ObjectPageSubSection titleText="Employee Details" id="employment-employee-details">
<Form>
<FormItem labelContent={<Label showColon>Start Date</Label>}>
<Text>Jan 01, 2018</Text>
Expand All @@ -303,11 +290,7 @@ export const Default: Story = {
</FormItem>
</Form>
</ObjectPageSubSection>
<ObjectPageSubSection
titleText="Job Relationship"
id="employment-job-relationship"
aria-label="Job Relationship"
>
<ObjectPageSubSection titleText="Job Relationship" id="employment-job-relationship">
<Form>
<FormItem labelContent={<Label showColon>Manager</Label>}>
<Text>John Doe</Text>
Expand Down Expand Up @@ -346,26 +329,18 @@ export const SectionWithCustomHeader: Story = {
render(args) {
return (
<ObjectPage {...args} onBeforeNavigate={args.onBeforeNavigate}>
<ObjectPageSection
titleText="Goals"
hideTitleText
id="goals"
aria-label="Goals"
header={<Title>Custom Header Section One</Title>}
>
<ObjectPageSection titleText="Goals" hideTitleText id="goals" header={<Title>Custom Header Section One</Title>}>
<div style={{ width: '100%', height: '200px', background: 'cadetblue' }} />
</ObjectPageSection>
<ObjectPageSection
titleText="Personal"
hideTitleText
id="personal"
aria-label="Personal"
header={<MessageStrip hideCloseButton>Custom Header Section Two</MessageStrip>}
>
<ObjectPageSubSection
titleText="Connect"
id="personal-connect"
aria-label="Connect"
actions={
<>
<Button design={ButtonDesign.Emphasized} style={{ minWidth: '120px' }}>
Expand Down Expand Up @@ -406,11 +381,7 @@ export const SectionWithCustomHeader: Story = {
</FormGroup>
</Form>
</ObjectPageSubSection>
<ObjectPageSubSection
titleText="Payment Information"
id="personal-payment-information"
aria-label="Payment Information"
>
<ObjectPageSubSection titleText="Payment Information" id="personal-payment-information">
<Form>
<FormGroup headerText="Salary">
<FormItem labelContent={<Label showColon>Bank Transfer</Label>}>
Expand All @@ -429,7 +400,6 @@ export const SectionWithCustomHeader: Story = {
titleText="Employment"
hideTitleText
id="employment"
aria-label="Employment"
header={
<div style={{ display: 'flex', alignItems: 'center' }}>
<Icon name={sunIcon} style={{ marginInlineEnd: '2rem' }} />
Expand Down Expand Up @@ -538,12 +508,8 @@ export const SingleSection: Story = {
render(args) {
return (
<ObjectPage {...args}>
<ObjectPageSection titleText="Employment" id="employment" aria-label="Employment">
<ObjectPageSubSection
titleText="Job Information"
id="employment-job-information"
aria-label="Job Information"
>
<ObjectPageSection titleText="Employment" id="employment">
<ObjectPageSubSection titleText="Job Information" id="employment-job-information">
<Form>
<FormItem labelContent={<Label showColon>Job Classification</Label>}>
<FlexBox direction={FlexBoxDirection.Column}>
Expand Down Expand Up @@ -571,11 +537,7 @@ export const SingleSection: Story = {
</FormItem>
</Form>
</ObjectPageSubSection>
<ObjectPageSubSection
titleText="Employee Details"
id="employment-employee-details"
aria-label="Employee Details"
>
<ObjectPageSubSection titleText="Employee Details" id="employment-employee-details">
<Form>
<FormItem labelContent={<Label showColon>Start Date</Label>}>
<Text>Jan 01, 2018</Text>
Expand All @@ -597,11 +559,7 @@ export const SingleSection: Story = {
</FormItem>
</Form>
</ObjectPageSubSection>
<ObjectPageSubSection
titleText="Job Relationship"
id="employment-job-relationship"
aria-label="Job Relationship"
>
<ObjectPageSubSection titleText="Job Relationship" id="employment-job-relationship">
<Form>
<FormItem labelContent={<Label showColon>Manager</Label>}>
<Text>John Doe</Text>
Expand Down
10 changes: 9 additions & 1 deletion packages/main/src/components/ObjectPage/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { createContext, useContext } from 'react';
import { ObjectPageMode } from '../../enums/ObjectPageMode.js';
import type { ObjectPagePropTypes } from './types/index.js';

export const ObjectPageContext = createContext<ObjectPagePropTypes['mode']>(ObjectPageMode.Default);
interface ObjectPageContextValue {
mode: ObjectPagePropTypes['mode'];
hasOnlySingleSection: boolean;
}

export const ObjectPageContext = createContext<ObjectPageContextValue>({
mode: ObjectPageMode.Default,
hasOnlySingleSection: false,
});

export const useObjectPageContext = () => useContext(ObjectPageContext);
18 changes: 14 additions & 4 deletions packages/main/src/components/ObjectPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
const [tabSelectId, setTabSelectId] = useState<null | string>(null);
const titleAreaInteractive = headerArea && !preserveHeaderStateOnClick;
const hasOnlySingleSection = Children.count(children) === 1;
const objectPageContextValue = useMemo(() => ({ mode, hasOnlySingleSection }), [mode, hasOnlySingleSection]);
const tabContainerHeaderHeight = hasOnlySingleSection ? 1 : TAB_CONTAINER_HEADER_HEIGHT;

const isProgrammaticallyScrolled = useRef(false);
Expand Down Expand Up @@ -716,15 +717,24 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
}

useEffect(() => {
if (isMounted && children && mode === ObjectPageMode.Default) {
if (isMounted && children && (mode === ObjectPageMode.Default || hasOnlySingleSection)) {
const firstSection: HTMLElement = objectPageContentRef.current.querySelector(
'[data-component-name="ObjectPageSection"]',
);
if (firstSection) {
firstSection.tabIndex = 0;
if (hasOnlySingleSection) {
const firstSubSection = firstSection.querySelector<HTMLElement>(
'[data-component-name="ObjectPageSubSection"]',
);
if (firstSubSection) {
firstSubSection.tabIndex = 0;
}
} else if (mode === ObjectPageMode.Default) {
firstSection.tabIndex = 0;
}
}
}
}, [isMounted, children, mode]);
}, [isMounted, children, mode, hasOnlySingleSection]);

const handleContentBlur: FocusEventHandler<HTMLDivElement> = (e) => {
const opNode = objectPageRef.current;
Expand All @@ -746,7 +756,7 @@ const ObjectPage = forwardRef<ObjectPageDomRef, ObjectPagePropTypes>((props, ref
};

return (
<ObjectPageContext.Provider value={mode}>
<ObjectPageContext.Provider value={objectPageContextValue}>
<div
ref={componentRef}
data-component-name="ObjectPage"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Title } from '../../../webComponents/Title/index.js';
import { FlexBox } from '../../FlexBox/index.js';
import { ObjectPageHeader } from '../../ObjectPageHeader/index.js';
import { ObjectPageSection } from '../../ObjectPageSection/index.js';
import { ObjectPageSubSection } from '../../ObjectPageSubSection/index.js';
import { ObjectPageTitle } from '../../ObjectPageTitle/index.js';
import { ObjectPage } from '../index.js';

Expand Down Expand Up @@ -114,3 +115,54 @@ export const ObjectPageShortSectionTabSwitchTestComp = () => {
</ObjectPage>
);
};

export const ObjectPageAccessibleNameTestComp = () => {
return (
<>
<span id="external-section-label">External section label</span>
<ObjectPageSection id="automatic" titleText="Automatic section">
<ObjectPageSubSection id="automatic-subsection" titleText="Automatic subsection">
Content
</ObjectPageSubSection>
</ObjectPageSection>
<ObjectPageSection id="aria-label" titleText="Section with aria-label" aria-label="Custom section label">
Content
</ObjectPageSection>
<ObjectPageSection
id="aria-labelledby"
titleText="Section with aria-labelledby"
aria-labelledby="external-section-label"
>
Content
</ObjectPageSection>
</>
);
};

export const ObjectPageSingleSectionTestComp = () => {
return (
<>
<ObjectPage>
<ObjectPageSection id="single" titleText="Single section">
<ObjectPageSubSection id="first" titleText="First subsection">
First subsection content
</ObjectPageSubSection>
<ObjectPageSubSection id="second" titleText="Second subsection">
Second subsection content
</ObjectPageSubSection>
</ObjectPageSection>
</ObjectPage>
<button type="button">After ObjectPage</button>
</>
);
};

export const ObjectPageSingleSectionIconTabBarTestComp = () => {
return (
<ObjectPage mode={ObjectPageMode.IconTabBar}>
<ObjectPageSection id="single" titleText="Single section">
Single section content
</ObjectPageSection>
</ObjectPage>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,53 @@ test.describe('ObjectPage', () => {
await page.getByRole('tab', { name: 'Short' }).click();
await expect.poll(topSpacerHeight).toBe(0);
});

test('uses titleText as the accessible name and respects explicit names', async ({ mount, page }) => {
await mount('ObjectPage/ObjectPageAccessibleNameTestComp');

const automaticSection = page.getByRole('region', { name: 'Automatic section' });
const automaticSubsection = page.getByRole('region', { name: 'Automatic subsection' });

await expect(automaticSection).toHaveAttribute('aria-label', 'Automatic section');
await expect(automaticSubsection).toHaveAttribute('aria-label', 'Automatic subsection');

await expect(page.getByRole('region', { name: 'Custom section label' })).toHaveAttribute(
'aria-label',
'Custom section label',
);
await expect(page.getByRole('region', { name: 'External section label' })).toHaveAttribute(
'aria-labelledby',
'external-section-label',
);
});

test('keeps only a single section out of the tab order', async ({ mount, page }) => {
await mount('ObjectPage/ObjectPageSingleSectionTestComp');

const section = page.getByRole('region', { name: 'Single section' });
const firstSubSection = page.getByRole('region', { name: 'First subsection' });
const secondSubSection = page.getByRole('region', { name: 'Second subsection' });

expect(await section.getAttribute('tabindex')).toBeNull();
await expect(section).toHaveAttribute('aria-label', 'Single section');
await expect(firstSubSection).toHaveAttribute('tabindex', '0');
await expect(page.locator('[data-component-name="ObjectPageTabContainer"]')).toHaveCount(0);

await firstSubSection.focus();
await page.keyboard.press('ArrowDown');
await expect(secondSubSection).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByRole('button', { name: 'After ObjectPage' })).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(secondSubSection).toBeFocused();
expect(await section.getAttribute('tabindex')).toBeNull();

await mount('ObjectPage/ObjectPageSingleSectionIconTabBarTestComp');
expect(await page.getByRole('region', { name: 'Single section' }).getAttribute('tabindex')).toBeNull();
await expect(page.locator('[data-component-name="ObjectPageTabContainer"]')).toHaveCount(0);

await mount('ObjectPage/ObjectPageFitContentTabSwitchTestComp');
await expect(page.getByRole('region', { name: 'Fit 1' })).toHaveAttribute('tabindex', '0');
await expect(page.locator('[data-component-name="ObjectPageTabContainer"]').first()).toBeVisible();
});
});
Loading
Loading