Skip to content

Commit b36f1fa

Browse files
authored
feat(ObjectPage): implement keyboard-navigation & focus handling for sections (#7528)
Closes #7386 Closes #7268 Closes #7548
1 parent 35b1fc8 commit b36f1fa

File tree

9 files changed

+760
-182
lines changed

9 files changed

+760
-182
lines changed

packages/main/src/components/ObjectPage/ObjectPage.cy.tsx

Lines changed: 308 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1433,6 +1433,311 @@ describe('ObjectPage', () => {
14331433
});
14341434

14351435
cypressPassThroughTestsFactory(ObjectPage);
1436+
1437+
it('focus behavior & keyboard navigation', () => {
1438+
cy.mount(
1439+
<>
1440+
<button data-testid="start">Start for tabbing chain</button>
1441+
<ObjectPage
1442+
data-testid="op"
1443+
titleArea={DPTitle}
1444+
headerArea={DPContent}
1445+
footerArea={Footer}
1446+
style={{ height: '700px' }}
1447+
>
1448+
<ObjectPageSection titleText="Goals" id="goals" aria-label="Goals">
1449+
<Form layout="S1 M2 L3 XL3" labelSpan="S12 M12 L12 XL12">
1450+
<FormItem labelContent={<Label showColon>Evangelize the UI framework across the company</Label>}>
1451+
<Text>4 days overdue - Cascaded</Text>
1452+
</FormItem>
1453+
<FormItem labelContent={<Label showColon>Get trained in development management direction</Label>}>
1454+
<Text>Due Nov, 21</Text>
1455+
</FormItem>
1456+
<FormItem labelContent={<Label showColon>Mentor junior developers</Label>}>
1457+
<Text>Due Dec, 31 - Cascaded</Text>
1458+
</FormItem>
1459+
</Form>
1460+
</ObjectPageSection>
1461+
<ObjectPageSection id={'dummy'} titleText={'Dummy'} aria-label={'Dummy'}>
1462+
Dummy
1463+
</ObjectPageSection>
1464+
<ObjectPageSection titleText="Personal" id="personal" aria-label="Personal">
1465+
<ObjectPageSubSection
1466+
titleText="Connect"
1467+
id="personal-connect"
1468+
aria-label="Connect"
1469+
actions={
1470+
<>
1471+
<Button design={ButtonDesign.Emphasized} data-testid="customAction">
1472+
Custom Action
1473+
</Button>
1474+
</>
1475+
}
1476+
>
1477+
<Form style={{ alignItems: 'baseline' }}>
1478+
<FormGroup headerText="Phone Numbers">
1479+
<FormItem labelContent={<Label showColon>Home</Label>}>
1480+
<Text>+1 234-567-8901</Text>
1481+
<Text>+1 234-567-5555</Text>
1482+
</FormItem>
1483+
</FormGroup>
1484+
<FormGroup headerText="Social Accounts">
1485+
<FormItem labelContent={<Label showColon>LinkedIn</Label>}>
1486+
<Text>/DeniseSmith</Text>
1487+
</FormItem>
1488+
<FormItem labelContent={<Label showColon>Twitter</Label>}>
1489+
<Text>@DeniseSmith</Text>
1490+
</FormItem>
1491+
</FormGroup>
1492+
<FormGroup headerText="Addresses">
1493+
<FormItem labelContent={<Label showColon>Home Address</Label>}>
1494+
<Text>2096 Mission Street</Text>
1495+
</FormItem>
1496+
<FormItem labelContent={<Label showColon>Mailing Address</Label>}>
1497+
<Text>PO Box 32114</Text>
1498+
</FormItem>
1499+
</FormGroup>
1500+
<FormGroup headerText="Mailing Address">
1501+
<FormItem labelContent={<Label showColon>Work</Label>}>
1502+
<Text>[email protected]</Text>
1503+
</FormItem>
1504+
</FormGroup>
1505+
</Form>
1506+
</ObjectPageSubSection>
1507+
<ObjectPageSubSection
1508+
titleText="Payment Information"
1509+
id="personal-payment-information"
1510+
aria-label="Payment Information"
1511+
>
1512+
<Form>
1513+
<FormGroup headerText="Salary">
1514+
<FormItem labelContent={<Label showColon>Bank Transfer</Label>}>
1515+
<Text>Money Bank, Inc.</Text>
1516+
</FormItem>
1517+
</FormGroup>
1518+
<FormGroup headerText="Payment method for Expenses">
1519+
<FormItem labelContent={<Label showColon>Extra Travel Expenses</Label>}>
1520+
<Text>Cash 100 USD</Text>
1521+
</FormItem>
1522+
</FormGroup>
1523+
</Form>
1524+
</ObjectPageSubSection>
1525+
</ObjectPageSection>
1526+
<ObjectPageSection titleText="Employment" id="employment" aria-label="Employment">
1527+
<ObjectPageSubSection
1528+
titleText="Job Information"
1529+
id="employment-job-information"
1530+
aria-label="Job Information"
1531+
>
1532+
<Form>
1533+
<FormItem labelContent={<Label showColon>Job Classification</Label>}>
1534+
<FlexBox direction={FlexBoxDirection.Column}>
1535+
<Text>Senior UI Developer</Text>
1536+
<Label>(UIDEV-SR)</Label>
1537+
</FlexBox>
1538+
</FormItem>
1539+
<FormItem labelContent={<Label showColon>Job Title</Label>}>
1540+
<Text>Developer</Text>
1541+
</FormItem>
1542+
<FormItem labelContent={<Label showColon>Employee Class</Label>}>
1543+
<Text>Employee</Text>
1544+
</FormItem>
1545+
<FormItem labelContent={<Label showColon>Manager</Label>}>
1546+
<FlexBox direction={FlexBoxDirection.Column}>
1547+
<Text>Dan Smith</Text>
1548+
<Label>Development Manager</Label>
1549+
</FlexBox>
1550+
</FormItem>
1551+
<FormItem labelContent={<Label showColon>Pay Grade</Label>}>
1552+
<Text>Salary Grade 18 (GR-14)</Text>
1553+
</FormItem>
1554+
<FormItem labelContent={<Label showColon>FTE</Label>}>
1555+
<Text>1</Text>
1556+
</FormItem>
1557+
</Form>
1558+
</ObjectPageSubSection>
1559+
<ObjectPageSubSection
1560+
titleText="Employee Details"
1561+
id="employment-employee-details"
1562+
aria-label="Employee Details"
1563+
>
1564+
<Form>
1565+
<FormItem labelContent={<Label showColon>Start Date</Label>}>
1566+
<Text>Jan 01, 2018</Text>
1567+
</FormItem>
1568+
<FormItem labelContent={<Label showColon>End Date</Label>}>
1569+
<Text>Dec 31, 9999</Text>
1570+
</FormItem>
1571+
<FormItem labelContent={<Label showColon>Payroll Start Date</Label>}>
1572+
<Text>Jan 01, 2018</Text>
1573+
</FormItem>
1574+
<FormItem labelContent={<Label showColon>Benefits Start Date</Label>}>
1575+
<Text>Jul 01, 2018</Text>
1576+
</FormItem>
1577+
<FormItem labelContent={<Label showColon>Company Car Eligibility</Label>}>
1578+
<Text>Jan 01, 2021</Text>
1579+
</FormItem>
1580+
<FormItem labelContent={<Label showColon>Equity Start Date</Label>}>
1581+
<Text>Jul 01, 2018</Text>
1582+
</FormItem>
1583+
</Form>
1584+
</ObjectPageSubSection>
1585+
<ObjectPageSubSection
1586+
titleText="Job Relationship"
1587+
id="employment-job-relationship"
1588+
aria-label="Job Relationship"
1589+
>
1590+
<Form>
1591+
<FormItem labelContent={<Label showColon>Manager</Label>}>
1592+
<Text>John Doe</Text>
1593+
</FormItem>
1594+
<FormItem labelContent={<Label showColon>Scrum Master</Label>}>
1595+
<Text>Michael Adams</Text>
1596+
</FormItem>
1597+
<FormItem labelContent={<Label showColon>Product Owner</Label>}>
1598+
<Text>John Miller</Text>
1599+
</FormItem>
1600+
</Form>
1601+
</ObjectPageSubSection>
1602+
</ObjectPageSection>
1603+
<ObjectPageSection id={'5'} titleText={'SingleSectionInput'} aria-label="SingleSectionInput">
1604+
<Input data-testid="single" />
1605+
</ObjectPageSection>
1606+
<ObjectPageSection id={'6'} titleText={'SubSectionsInput'} aria-label="SubSectionsInput">
1607+
<ObjectPageSubSection id="6.1" titleText="6.1" aria-label="6.1">
1608+
Some Text
1609+
</ObjectPageSubSection>
1610+
<ObjectPageSubSection id="6.2" titleText="6.2" aria-label="6.2">
1611+
<Input data-testid="sub" />
1612+
</ObjectPageSubSection>
1613+
</ObjectPageSection>
1614+
</ObjectPage>
1615+
</>,
1616+
);
1617+
1618+
cy.get('[data-component-name="ObjectPageSection"]').as('sections');
1619+
cy.get('@sections').eq(0).should('have.attr', 'tabindex', 0);
1620+
cy.get('@sections').each((section, index) => {
1621+
if (index !== 0) {
1622+
cy.wrap(section).should('have.attr', 'tabindex', -1);
1623+
}
1624+
});
1625+
cy.get('[data-component-name="ObjectPageSubSection"]').should('have.attr', 'tabindex', -1);
1626+
1627+
cy.findByTestId('start').focus();
1628+
// breadcrumbs
1629+
cy.realPress('Tab');
1630+
//toolbar
1631+
cy.realPress('Tab');
1632+
cy.realPress('Tab');
1633+
// header content (links)
1634+
cy.realPress('Tab');
1635+
cy.realPress('Tab');
1636+
cy.realPress('Tab');
1637+
// anchor buttons
1638+
cy.realPress('Tab');
1639+
cy.realPress('Tab');
1640+
// tabbar
1641+
cy.realPress('Tab');
1642+
// first section
1643+
cy.realPress('Tab');
1644+
cy.focused().should('have.attr', 'aria-label', 'Goals').and('have.attr', 'tabindex', 0);
1645+
// Personal: custom action
1646+
cy.realPress('Tab');
1647+
cy.findByTestId('customAction').should('be.focused');
1648+
// SingleSectionInput
1649+
cy.realPress('Tab');
1650+
cy.findByTestId('single').should('be.focused');
1651+
// 6.2 input
1652+
cy.realPress('Tab');
1653+
cy.findByTestId('sub').should('be.focused');
1654+
//footer
1655+
cy.realPress('Tab');
1656+
cy.findByTestId('footer-accept-btn').should('be.focused');
1657+
// 6.2 input
1658+
cy.realPress(['Shift', 'Tab']);
1659+
cy.findByTestId('sub').should('be.focused');
1660+
// 6.2 subsection
1661+
cy.realPress(['Shift', 'Tab']);
1662+
cy.focused().should('have.attr', 'aria-label', '6.2').and('have.attr', 'tabindex', 0);
1663+
// SubSectionsInput
1664+
cy.realPress(['Shift', 'Tab']);
1665+
cy.focused().should('have.attr', 'aria-label', 'SubSectionsInput').and('have.attr', 'tabindex', 0);
1666+
// SingleSectionInput
1667+
cy.realPress(['Shift', 'Tab']);
1668+
cy.findByTestId('single').should('be.focused');
1669+
// section SingleSectionInput
1670+
cy.realPress(['Shift', 'Tab']);
1671+
cy.focused().should('have.attr', 'aria-label', 'SingleSectionInput').and('have.attr', 'tabindex', 0);
1672+
// Personal: custom action btn
1673+
cy.realPress(['Shift', 'Tab']);
1674+
cy.findByTestId('customAction').should('be.focused');
1675+
// Personal: Connect - subsection
1676+
cy.realPress(['Shift', 'Tab']);
1677+
cy.focused().should('have.attr', 'aria-label', 'Connect').and('have.attr', 'tabindex', 0);
1678+
// Personal: section
1679+
cy.realPress(['Shift', 'Tab']);
1680+
cy.focused().should('have.attr', 'aria-label', 'Personal').and('have.attr', 'tabindex', 0);
1681+
// tabbar
1682+
cy.realPress(['Shift', 'Tab']);
1683+
1684+
cy.get('@sections').eq(2).should('have.attr', 'tabindex', 0);
1685+
cy.get('@sections').each((section, index) => {
1686+
if (index !== 2) {
1687+
cy.wrap(section).should('have.attr', 'tabindex', -1);
1688+
}
1689+
});
1690+
cy.get('[data-component-name="ObjectPageSubSection"]').should('have.attr', 'tabindex', -1);
1691+
1692+
// click first Tab
1693+
cy.focused().realClick();
1694+
cy.focused().should('have.attr', 'aria-label', 'Goals').and('have.attr', 'tabindex', 0);
1695+
1696+
// arrow section navigation
1697+
cy.realPress('ArrowUp');
1698+
cy.focused().should('have.attr', 'aria-label', 'Goals').and('have.attr', 'tabindex', 0);
1699+
cy.realPress('ArrowDown');
1700+
cy.focused().should('have.attr', 'aria-label', 'Dummy').and('have.attr', 'tabindex', 0);
1701+
cy.realPress('ArrowDown');
1702+
cy.focused().should('have.attr', 'aria-label', 'Personal').and('have.attr', 'tabindex', 0);
1703+
cy.realPress('ArrowDown');
1704+
cy.focused().should('have.attr', 'aria-label', 'Employment').and('have.attr', 'tabindex', 0);
1705+
cy.realPress('ArrowDown');
1706+
cy.focused().should('have.attr', 'aria-label', 'SingleSectionInput').and('have.attr', 'tabindex', 0);
1707+
cy.realPress('ArrowDown');
1708+
cy.focused().should('have.attr', 'aria-label', 'SubSectionsInput').and('have.attr', 'tabindex', 0);
1709+
cy.realPress('ArrowDown');
1710+
cy.focused().should('have.attr', 'aria-label', 'SubSectionsInput').and('have.attr', 'tabindex', 0);
1711+
1712+
// arrow subsection navigation
1713+
cy.realPress('Tab');
1714+
cy.focused().should('have.attr', 'aria-label', '6.1').and('have.attr', 'tabindex', 0);
1715+
cy.realPress('ArrowUp');
1716+
cy.focused().should('have.attr', 'aria-label', '6.1').and('have.attr', 'tabindex', 0);
1717+
cy.realPress('ArrowDown');
1718+
cy.focused().should('have.attr', 'aria-label', '6.2').and('have.attr', 'tabindex', 0);
1719+
1720+
cy.get('[ui5-tabcontainer]').findUi5TabOpenPopoverButtonByText('Employment').click();
1721+
cy.get('[ui5-responsive-popover]').should('be.visible');
1722+
cy.realPress('ArrowDown');
1723+
cy.realPress('Enter');
1724+
cy.focused().should('have.attr', 'aria-label', 'Employee Details').and('have.attr', 'tabindex', 0);
1725+
1726+
cy.get('[data-component-name="ObjectPageSection"]').as('sections');
1727+
cy.get('@sections').eq(3).should('have.attr', 'tabindex', 0);
1728+
cy.get('@sections').each((section, index) => {
1729+
if (index !== 3) {
1730+
cy.wrap(section).should('have.attr', 'tabindex', -1);
1731+
}
1732+
});
1733+
cy.get('[data-component-name="ObjectPageSubSection"]').as('subsections');
1734+
cy.get('@subsections').eq(3).should('have.attr', 'tabindex', 0);
1735+
cy.get('@subsections').each((section, index) => {
1736+
if (index !== 3) {
1737+
cy.wrap(section).should('have.attr', 'tabindex', -1);
1738+
}
1739+
});
1740+
});
14361741
});
14371742

14381743
const DPTitle = (
@@ -1602,7 +1907,9 @@ const Footer = (
16021907
design={BarDesign.FloatingFooter}
16031908
endContent={
16041909
<>
1605-
<Button design={ButtonDesign.Positive}>Accept</Button>
1910+
<Button design={ButtonDesign.Positive} data-testid="footer-accept-btn">
1911+
Accept
1912+
</Button>
16061913
<Button design={ButtonDesign.Negative}>Reject</Button>
16071914
</>
16081915
}

packages/main/src/components/ObjectPage/ObjectPage.module.css

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,7 @@
130130

131131
@container (max-width: 599px) {
132132
.header,
133-
.headerContainer,
134-
.content {
133+
.headerContainer {
135134
padding-inline: 1rem;
136135
}
137136

@@ -142,8 +141,7 @@
142141

143142
@container (min-width: 600px) and (max-width: 1439px) {
144143
.header,
145-
.headerContainer,
146-
.content {
144+
.headerContainer {
147145
padding-inline: 2rem;
148146
}
149147

@@ -154,8 +152,7 @@
154152

155153
@container (min-width: 1440px) {
156154
.header,
157-
.headerContainer,
158-
.content {
155+
.headerContainer {
159156
padding-inline: 3rem;
160157
}
161158

0 commit comments

Comments
 (0)