@instructure/ui-breadcrumb 10.10.1-snapshot-1 → 10.10.1-snapshot-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,9 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [10.10.1-snapshot-1](https://github.com/instructure/instructure-ui/compare/v10.10.0...v10.10.1-snapshot-1) (2025-01-08)
6
+ ## [10.10.1-snapshot-3](https://github.com/instructure/instructure-ui/compare/v10.10.0...v10.10.1-snapshot-3) (2025-01-10)
7
7
 
8
- **Note:** Version bump only for package @instructure/ui-breadcrumb
8
+
9
+ ### Bug Fixes
10
+
11
+ * **ui-breadcrumb:** add and update aria tags in Breadcrumb and in documentation ([e8db0f2](https://github.com/instructure/instructure-ui/commit/e8db0f20b5f8acc6662a1c8cc7c2530466e6869d))
9
12
 
10
13
 
11
14
 
@@ -50,7 +50,8 @@ let BreadcrumbLink = (_dec = testable(), _dec(_class = (_BreadcrumbLink = class
50
50
  renderIcon = _this$props.renderIcon,
51
51
  iconPlacement = _this$props.iconPlacement,
52
52
  onClick = _this$props.onClick,
53
- onMouseEnter = _this$props.onMouseEnter;
53
+ onMouseEnter = _this$props.onMouseEnter,
54
+ isCurrentPage = _this$props.isCurrentPage;
54
55
  const props = omitProps(this.props, BreadcrumbLink.allowedProps);
55
56
  return /*#__PURE__*/React.createElement(Link, Object.assign({}, props, {
56
57
  href: href,
@@ -60,6 +61,8 @@ let BreadcrumbLink = (_dec = testable(), _dec(_class = (_BreadcrumbLink = class
60
61
  onMouseEnter: onMouseEnter,
61
62
  isWithinText: false,
62
63
  elementRef: this.handleRef
64
+ }, isCurrentPage && {
65
+ 'aria-current': 'page'
63
66
  }), /*#__PURE__*/React.createElement(TruncateText, null, children));
64
67
  }
65
68
  }, _BreadcrumbLink.displayName = "BreadcrumbLink", _BreadcrumbLink.componentId = 'Breadcrumb.Link', _BreadcrumbLink.propTypes = propTypes, _BreadcrumbLink.allowedProps = allowedProps, _BreadcrumbLink.defaultProps = {}, _BreadcrumbLink)) || _class);
@@ -30,7 +30,8 @@ const propTypes = {
30
30
  onMouseEnter: PropTypes.func,
31
31
  size: PropTypes.oneOf(['small', 'medium', 'large']),
32
32
  renderIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
33
- iconPlacement: PropTypes.oneOf(['start', 'end'])
33
+ iconPlacement: PropTypes.oneOf(['start', 'end']),
34
+ isCurrentPage: PropTypes.bool
34
35
  };
35
- const allowedProps = ['children', 'href', 'iconPlacement', 'onClick', 'onMouseEnter', 'renderIcon', 'size'];
36
+ const allowedProps = ['children', 'href', 'iconPlacement', 'onClick', 'onMouseEnter', 'renderIcon', 'size', 'isCurrentPage'];
36
37
  export { propTypes, allowedProps };
@@ -1,4 +1,4 @@
1
- var _Breadcrumb, _Breadcrumb2, _Breadcrumb3;
1
+ var _Breadcrumb, _Breadcrumb2, _Breadcrumb3, _Breadcrumb4, _Breadcrumb5, _Breadcrumb6, _Breadcrumb7;
2
2
  /*
3
3
  * The MIT License (MIT)
4
4
  *
@@ -89,4 +89,57 @@ describe('<Breadcrumb />', () => {
89
89
  expect(icon).toBeInTheDocument();
90
90
  expect(icon).toHaveAttribute('aria-hidden', 'true');
91
91
  });
92
+ it('should add aria-current="page" to the last element by default', () => {
93
+ const _render4 = render(_Breadcrumb4 || (_Breadcrumb4 = /*#__PURE__*/React.createElement(Breadcrumb, {
94
+ label: TEST_LABEL
95
+ }, /*#__PURE__*/React.createElement(Breadcrumb.Link, {
96
+ href: TEST_LINK
97
+ }, TEST_TEXT_01), /*#__PURE__*/React.createElement(Breadcrumb.Link, null, TEST_TEXT_02)))),
98
+ container = _render4.container;
99
+ const links = container.querySelectorAll('[class$="--block-link"]');
100
+ const firstLink = links[0];
101
+ const lastLink = links[links.length - 1];
102
+ expect(firstLink).not.toHaveAttribute('aria-current', 'page');
103
+ expect(lastLink).toHaveAttribute('aria-current', 'page');
104
+ });
105
+ it('should add aria-current="page" to the element if isCurrent is true', () => {
106
+ const _render5 = render(_Breadcrumb5 || (_Breadcrumb5 = /*#__PURE__*/React.createElement(Breadcrumb, {
107
+ label: TEST_LABEL
108
+ }, /*#__PURE__*/React.createElement(Breadcrumb.Link, {
109
+ isCurrentPage: true,
110
+ href: TEST_LINK
111
+ }, TEST_TEXT_01), /*#__PURE__*/React.createElement(Breadcrumb.Link, null, TEST_TEXT_02)))),
112
+ container = _render5.container;
113
+ const links = container.querySelectorAll('[class$="--block-link"]');
114
+ const firstLink = links[0];
115
+ const lastLink = links[links.length - 1];
116
+ expect(firstLink).toHaveAttribute('aria-current', 'page');
117
+ expect(lastLink).not.toHaveAttribute('aria-current', 'page');
118
+ });
119
+ it('should throw a warning when multiple elements have isCurrent set to true ', () => {
120
+ render(_Breadcrumb6 || (_Breadcrumb6 = /*#__PURE__*/React.createElement(Breadcrumb, {
121
+ label: TEST_LABEL
122
+ }, /*#__PURE__*/React.createElement(Breadcrumb.Link, {
123
+ isCurrentPage: true,
124
+ href: TEST_LINK
125
+ }, TEST_TEXT_01), /*#__PURE__*/React.createElement(Breadcrumb.Link, {
126
+ isCurrentPage: true
127
+ }, TEST_TEXT_02))));
128
+ expect(consoleWarningMock).toHaveBeenCalledWith(expect.stringContaining('Warning: Multiple elements with isCurrentPage=true found. Only one element should be set to current.'));
129
+ });
130
+ it('should not add aria-current="page" to the last element if it set to false', () => {
131
+ const _render6 = render(_Breadcrumb7 || (_Breadcrumb7 = /*#__PURE__*/React.createElement(Breadcrumb, {
132
+ label: TEST_LABEL
133
+ }, /*#__PURE__*/React.createElement(Breadcrumb.Link, {
134
+ href: TEST_LINK
135
+ }, TEST_TEXT_01), /*#__PURE__*/React.createElement(Breadcrumb.Link, {
136
+ isCurrentPage: false
137
+ }, TEST_TEXT_02)))),
138
+ container = _render6.container;
139
+ const links = container.querySelectorAll('[class$="--block-link"]');
140
+ const firstLink = links[0];
141
+ const lastLink = links[links.length - 1];
142
+ expect(firstLink).not.toHaveAttribute('aria-current', 'page');
143
+ expect(lastLink).not.toHaveAttribute('aria-current', 'page');
144
+ });
92
145
  });
@@ -45,6 +45,12 @@ let Breadcrumb = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2
45
45
  this.handleRef = el => {
46
46
  this.ref = el;
47
47
  };
48
+ this.addAriaCurrent = child => {
49
+ const updatedChild = /*#__PURE__*/React.cloneElement(child, {
50
+ 'aria-current': 'page'
51
+ });
52
+ return updatedChild;
53
+ };
48
54
  }
49
55
  componentDidMount() {
50
56
  var _this$props$makeStyle, _this$props;
@@ -62,11 +68,20 @@ let Breadcrumb = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2
62
68
  const inlineStyle = {
63
69
  maxWidth: `${Math.floor(100 / numChildren)}%`
64
70
  };
71
+ let isAriaCurrentSet = false;
65
72
  return React.Children.map(children, (child, index) => {
73
+ const isLastElement = index === numChildren - 1;
74
+ if (/*#__PURE__*/React.isValidElement(child)) {
75
+ const isCurrentPage = child.props.isCurrentPage || false;
76
+ if (isAriaCurrentSet && isCurrentPage) {}
77
+ if (isCurrentPage) {
78
+ isAriaCurrentSet = true;
79
+ }
80
+ }
66
81
  return jsx("li", {
67
82
  css: styles === null || styles === void 0 ? void 0 : styles.crumb,
68
83
  style: inlineStyle
69
- }, child, index < numChildren - 1 && jsx(IconArrowOpenEndSolid, {
84
+ }, !isAriaCurrentSet && isLastElement && child.props.isCurrentPage !== false ? this.addAriaCurrent(child) : child, index < numChildren - 1 && jsx(IconArrowOpenEndSolid, {
70
85
  color: "auto",
71
86
  css: styles === null || styles === void 0 ? void 0 : styles.separator
72
87
  }));
@@ -56,7 +56,8 @@ let BreadcrumbLink = exports.BreadcrumbLink = (_dec = (0, _testable.testable)(),
56
56
  renderIcon = _this$props.renderIcon,
57
57
  iconPlacement = _this$props.iconPlacement,
58
58
  onClick = _this$props.onClick,
59
- onMouseEnter = _this$props.onMouseEnter;
59
+ onMouseEnter = _this$props.onMouseEnter,
60
+ isCurrentPage = _this$props.isCurrentPage;
60
61
  const props = (0, _omitProps.omitProps)(this.props, BreadcrumbLink.allowedProps);
61
62
  return /*#__PURE__*/_react.default.createElement(_Link.Link, Object.assign({}, props, {
62
63
  href: href,
@@ -66,6 +67,8 @@ let BreadcrumbLink = exports.BreadcrumbLink = (_dec = (0, _testable.testable)(),
66
67
  onMouseEnter: onMouseEnter,
67
68
  isWithinText: false,
68
69
  elementRef: this.handleRef
70
+ }, isCurrentPage && {
71
+ 'aria-current': 'page'
69
72
  }), /*#__PURE__*/_react.default.createElement(_TruncateText.TruncateText, null, children));
70
73
  }
71
74
  }, _BreadcrumbLink.displayName = "BreadcrumbLink", _BreadcrumbLink.componentId = 'Breadcrumb.Link', _BreadcrumbLink.propTypes = _props.propTypes, _BreadcrumbLink.allowedProps = _props.allowedProps, _BreadcrumbLink.defaultProps = {}, _BreadcrumbLink)) || _class);
@@ -37,6 +37,7 @@ const propTypes = exports.propTypes = {
37
37
  onMouseEnter: _propTypes.default.func,
38
38
  size: _propTypes.default.oneOf(['small', 'medium', 'large']),
39
39
  renderIcon: _propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.func]),
40
- iconPlacement: _propTypes.default.oneOf(['start', 'end'])
40
+ iconPlacement: _propTypes.default.oneOf(['start', 'end']),
41
+ isCurrentPage: _propTypes.default.bool
41
42
  };
42
- const allowedProps = exports.allowedProps = ['children', 'href', 'iconPlacement', 'onClick', 'onMouseEnter', 'renderIcon', 'size'];
43
+ const allowedProps = exports.allowedProps = ['children', 'href', 'iconPlacement', 'onClick', 'onMouseEnter', 'renderIcon', 'size', 'isCurrentPage'];
@@ -8,8 +8,8 @@ require("@testing-library/jest-dom");
8
8
  var _generateA11yTests = require("@instructure/ui-scripts/lib/test/generateA11yTests");
9
9
  var _runAxeCheck = require("@instructure/ui-axe-check/lib/runAxeCheck.js");
10
10
  var _index = require("../index");
11
- var _Breadcrumb4 = _interopRequireDefault(require("../__examples__/Breadcrumb.examples"));
12
- var _Breadcrumb, _Breadcrumb2, _Breadcrumb3;
11
+ var _Breadcrumb8 = _interopRequireDefault(require("../__examples__/Breadcrumb.examples"));
12
+ var _Breadcrumb, _Breadcrumb2, _Breadcrumb3, _Breadcrumb4, _Breadcrumb5, _Breadcrumb6, _Breadcrumb7;
13
13
  /*
14
14
  * The MIT License (MIT)
15
15
  *
@@ -61,7 +61,7 @@ describe('<Breadcrumb />', () => {
61
61
  expect(axeCheck).toBe(true);
62
62
  });
63
63
  describe('with generated examples', () => {
64
- const generatedComponents = (0, _generateA11yTests.generateA11yTests)(_index.Breadcrumb, _Breadcrumb4.default);
64
+ const generatedComponents = (0, _generateA11yTests.generateA11yTests)(_index.Breadcrumb, _Breadcrumb8.default);
65
65
  it.each(generatedComponents)('should be accessible with example: $description', async ({
66
66
  content
67
67
  }) => {
@@ -90,4 +90,57 @@ describe('<Breadcrumb />', () => {
90
90
  expect(icon).toBeInTheDocument();
91
91
  expect(icon).toHaveAttribute('aria-hidden', 'true');
92
92
  });
93
+ it('should add aria-current="page" to the last element by default', () => {
94
+ const _render4 = (0, _react2.render)(_Breadcrumb4 || (_Breadcrumb4 = /*#__PURE__*/_react.default.createElement(_index.Breadcrumb, {
95
+ label: TEST_LABEL
96
+ }, /*#__PURE__*/_react.default.createElement(_index.Breadcrumb.Link, {
97
+ href: TEST_LINK
98
+ }, TEST_TEXT_01), /*#__PURE__*/_react.default.createElement(_index.Breadcrumb.Link, null, TEST_TEXT_02)))),
99
+ container = _render4.container;
100
+ const links = container.querySelectorAll('[class$="--block-link"]');
101
+ const firstLink = links[0];
102
+ const lastLink = links[links.length - 1];
103
+ expect(firstLink).not.toHaveAttribute('aria-current', 'page');
104
+ expect(lastLink).toHaveAttribute('aria-current', 'page');
105
+ });
106
+ it('should add aria-current="page" to the element if isCurrent is true', () => {
107
+ const _render5 = (0, _react2.render)(_Breadcrumb5 || (_Breadcrumb5 = /*#__PURE__*/_react.default.createElement(_index.Breadcrumb, {
108
+ label: TEST_LABEL
109
+ }, /*#__PURE__*/_react.default.createElement(_index.Breadcrumb.Link, {
110
+ isCurrentPage: true,
111
+ href: TEST_LINK
112
+ }, TEST_TEXT_01), /*#__PURE__*/_react.default.createElement(_index.Breadcrumb.Link, null, TEST_TEXT_02)))),
113
+ container = _render5.container;
114
+ const links = container.querySelectorAll('[class$="--block-link"]');
115
+ const firstLink = links[0];
116
+ const lastLink = links[links.length - 1];
117
+ expect(firstLink).toHaveAttribute('aria-current', 'page');
118
+ expect(lastLink).not.toHaveAttribute('aria-current', 'page');
119
+ });
120
+ it('should throw a warning when multiple elements have isCurrent set to true ', () => {
121
+ (0, _react2.render)(_Breadcrumb6 || (_Breadcrumb6 = /*#__PURE__*/_react.default.createElement(_index.Breadcrumb, {
122
+ label: TEST_LABEL
123
+ }, /*#__PURE__*/_react.default.createElement(_index.Breadcrumb.Link, {
124
+ isCurrentPage: true,
125
+ href: TEST_LINK
126
+ }, TEST_TEXT_01), /*#__PURE__*/_react.default.createElement(_index.Breadcrumb.Link, {
127
+ isCurrentPage: true
128
+ }, TEST_TEXT_02))));
129
+ expect(consoleWarningMock).toHaveBeenCalledWith(expect.stringContaining('Warning: Multiple elements with isCurrentPage=true found. Only one element should be set to current.'));
130
+ });
131
+ it('should not add aria-current="page" to the last element if it set to false', () => {
132
+ const _render6 = (0, _react2.render)(_Breadcrumb7 || (_Breadcrumb7 = /*#__PURE__*/_react.default.createElement(_index.Breadcrumb, {
133
+ label: TEST_LABEL
134
+ }, /*#__PURE__*/_react.default.createElement(_index.Breadcrumb.Link, {
135
+ href: TEST_LINK
136
+ }, TEST_TEXT_01), /*#__PURE__*/_react.default.createElement(_index.Breadcrumb.Link, {
137
+ isCurrentPage: false
138
+ }, TEST_TEXT_02)))),
139
+ container = _render6.container;
140
+ const links = container.querySelectorAll('[class$="--block-link"]');
141
+ const firstLink = links[0];
142
+ const lastLink = links[links.length - 1];
143
+ expect(firstLink).not.toHaveAttribute('aria-current', 'page');
144
+ expect(lastLink).not.toHaveAttribute('aria-current', 'page');
145
+ });
93
146
  });
@@ -59,6 +59,12 @@ let Breadcrumb = exports.Breadcrumb = (_dec = (0, _emotion.withStyle)(_styles.de
59
59
  this.handleRef = el => {
60
60
  this.ref = el;
61
61
  };
62
+ this.addAriaCurrent = child => {
63
+ const updatedChild = /*#__PURE__*/_react.default.cloneElement(child, {
64
+ 'aria-current': 'page'
65
+ });
66
+ return updatedChild;
67
+ };
62
68
  }
63
69
  componentDidMount() {
64
70
  var _this$props$makeStyle, _this$props;
@@ -76,11 +82,20 @@ let Breadcrumb = exports.Breadcrumb = (_dec = (0, _emotion.withStyle)(_styles.de
76
82
  const inlineStyle = {
77
83
  maxWidth: `${Math.floor(100 / numChildren)}%`
78
84
  };
85
+ let isAriaCurrentSet = false;
79
86
  return _react.default.Children.map(children, (child, index) => {
87
+ const isLastElement = index === numChildren - 1;
88
+ if (/*#__PURE__*/_react.default.isValidElement(child)) {
89
+ const isCurrentPage = child.props.isCurrentPage || false;
90
+ if (isAriaCurrentSet && isCurrentPage) {}
91
+ if (isCurrentPage) {
92
+ isAriaCurrentSet = true;
93
+ }
94
+ }
80
95
  return (0, _emotion.jsx)("li", {
81
96
  css: styles === null || styles === void 0 ? void 0 : styles.crumb,
82
97
  style: inlineStyle
83
- }, child, index < numChildren - 1 && (0, _emotion.jsx)(_IconArrowOpenEndSolid.IconArrowOpenEndSolid, {
98
+ }, !isAriaCurrentSet && isLastElement && child.props.isCurrentPage !== false ? this.addAriaCurrent(child) : child, index < numChildren - 1 && (0, _emotion.jsx)(_IconArrowOpenEndSolid.IconArrowOpenEndSolid, {
84
99
  color: "auto",
85
100
  css: styles === null || styles === void 0 ? void 0 : styles.separator
86
101
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-breadcrumb",
3
- "version": "10.10.1-snapshot-1",
3
+ "version": "10.10.1-snapshot-3",
4
4
  "description": "A breadcrumb component",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -23,26 +23,26 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "devDependencies": {
26
- "@instructure/ui-axe-check": "10.10.1-snapshot-1",
27
- "@instructure/ui-babel-preset": "10.10.1-snapshot-1",
28
- "@instructure/ui-scripts": "10.10.1-snapshot-1",
29
- "@instructure/ui-themes": "10.10.1-snapshot-1",
26
+ "@instructure/ui-axe-check": "10.10.1-snapshot-3",
27
+ "@instructure/ui-babel-preset": "10.10.1-snapshot-3",
28
+ "@instructure/ui-scripts": "10.10.1-snapshot-3",
29
+ "@instructure/ui-themes": "10.10.1-snapshot-3",
30
30
  "@testing-library/jest-dom": "^6.6.3",
31
31
  "@testing-library/react": "^16.0.1",
32
32
  "vitest": "^2.1.8"
33
33
  },
34
34
  "dependencies": {
35
35
  "@babel/runtime": "^7.26.0",
36
- "@instructure/emotion": "10.10.1-snapshot-1",
37
- "@instructure/shared-types": "10.10.1-snapshot-1",
38
- "@instructure/ui-icons": "10.10.1-snapshot-1",
39
- "@instructure/ui-link": "10.10.1-snapshot-1",
40
- "@instructure/ui-prop-types": "10.10.1-snapshot-1",
41
- "@instructure/ui-react-utils": "10.10.1-snapshot-1",
42
- "@instructure/ui-testable": "10.10.1-snapshot-1",
43
- "@instructure/ui-truncate-text": "10.10.1-snapshot-1",
44
- "@instructure/ui-utils": "10.10.1-snapshot-1",
45
- "@instructure/ui-view": "10.10.1-snapshot-1",
36
+ "@instructure/emotion": "10.10.1-snapshot-3",
37
+ "@instructure/shared-types": "10.10.1-snapshot-3",
38
+ "@instructure/ui-icons": "10.10.1-snapshot-3",
39
+ "@instructure/ui-link": "10.10.1-snapshot-3",
40
+ "@instructure/ui-prop-types": "10.10.1-snapshot-3",
41
+ "@instructure/ui-react-utils": "10.10.1-snapshot-3",
42
+ "@instructure/ui-testable": "10.10.1-snapshot-3",
43
+ "@instructure/ui-truncate-text": "10.10.1-snapshot-3",
44
+ "@instructure/ui-utils": "10.10.1-snapshot-3",
45
+ "@instructure/ui-view": "10.10.1-snapshot-3",
46
46
  "prop-types": "^15.8.1"
47
47
  },
48
48
  "peerDependencies": {
@@ -54,8 +54,15 @@ class BreadcrumbLink extends Component<BreadcrumbLinkProps> {
54
54
  }
55
55
 
56
56
  render() {
57
- const { children, href, renderIcon, iconPlacement, onClick, onMouseEnter } =
58
- this.props
57
+ const {
58
+ children,
59
+ href,
60
+ renderIcon,
61
+ iconPlacement,
62
+ onClick,
63
+ onMouseEnter,
64
+ isCurrentPage
65
+ } = this.props
59
66
 
60
67
  const props = omitProps(this.props, BreadcrumbLink.allowedProps)
61
68
 
@@ -69,6 +76,7 @@ class BreadcrumbLink extends Component<BreadcrumbLinkProps> {
69
76
  onMouseEnter={onMouseEnter}
70
77
  isWithinText={false}
71
78
  elementRef={this.handleRef}
79
+ {...(isCurrentPage && { 'aria-current': 'page' })}
72
80
  >
73
81
  <TruncateText>{children}</TruncateText>
74
82
  </Link>
@@ -63,6 +63,11 @@ type BreadcrumbLinkOwnProps = {
63
63
  * Place the icon before or after the text in the Breadcrumb.Link
64
64
  */
65
65
  iconPlacement?: 'start' | 'end'
66
+ /**
67
+ * Whether the page this breadcrumb points to is the current one. If true, it sets aria-current="page".
68
+ * If this prop is not set to true on any breadcrumb element, the one recieving the aria-current="page" will always be the last element, unless the last element's isCurrentPage prop is explicity set to false.
69
+ */
70
+ isCurrentPage?: boolean
66
71
  }
67
72
 
68
73
  type PropKeys = keyof BreadcrumbLinkOwnProps
@@ -89,7 +94,8 @@ const propTypes: PropValidators<PropKeys> = {
89
94
  onMouseEnter: PropTypes.func,
90
95
  size: PropTypes.oneOf(['small', 'medium', 'large']),
91
96
  renderIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
92
- iconPlacement: PropTypes.oneOf(['start', 'end'])
97
+ iconPlacement: PropTypes.oneOf(['start', 'end']),
98
+ isCurrentPage: PropTypes.bool
93
99
  }
94
100
 
95
101
  const allowedProps: AllowedPropKeys = [
@@ -99,7 +105,8 @@ const allowedProps: AllowedPropKeys = [
99
105
  'onClick',
100
106
  'onMouseEnter',
101
107
  'renderIcon',
102
- 'size'
108
+ 'size',
109
+ 'isCurrentPage'
103
110
  ]
104
111
 
105
112
  export type { BreadcrumbLinkProps }
@@ -23,7 +23,7 @@ type: example
23
23
  {(props, matches) => {
24
24
  if (matches.includes('tablet')) {
25
25
  return (
26
- <Breadcrumb label="You are here:">
26
+ <Breadcrumb label="breadcrumb">
27
27
  <Breadcrumb.Link href="#">Student Forecast</Breadcrumb.Link>
28
28
  <Breadcrumb.Link href="#">University of Utah</Breadcrumb.Link>
29
29
  <Breadcrumb.Link href="#">University of Utah Colleges</Breadcrumb.Link>
@@ -52,7 +52,7 @@ Change the `size` prop to control the font-size of the breadcrumbs (default is `
52
52
  type: example
53
53
  ---
54
54
  <div>
55
- <Breadcrumb size="small" label="You are here:" margin="none none medium">
55
+ <Breadcrumb size="small" label="breadcrumb" margin="none none medium">
56
56
  <Breadcrumb.Link href="https://instructure.github.io/instructure-ui/">English 204</Breadcrumb.Link>
57
57
  <Breadcrumb.Link
58
58
  onClick={function () {
@@ -65,7 +65,7 @@ type: example
65
65
  <Breadcrumb.Link>Rabbit Is Rich</Breadcrumb.Link>
66
66
  </Breadcrumb>
67
67
  <View as="div" width="40rem">
68
- <Breadcrumb label="You are here:" margin="none none medium">
68
+ <Breadcrumb label="breadcrumb" margin="none none medium">
69
69
  <Breadcrumb.Link href="https://instructure.github.io/instructure-ui/">English 204</Breadcrumb.Link>
70
70
  <Breadcrumb.Link
71
71
  onClick={function () {
@@ -78,7 +78,7 @@ type: example
78
78
  <Breadcrumb.Link>Rabbit Is Rich</Breadcrumb.Link>
79
79
  </Breadcrumb>
80
80
  </View>
81
- <Breadcrumb size="large" label="You are here:">
81
+ <Breadcrumb size="large" label="breadcrumb">
82
82
  <Breadcrumb.Link href="https://instructure.github.io/instructure-ui/">English 204</Breadcrumb.Link>
83
83
  <Breadcrumb.Link
84
84
  onClick={function () {
@@ -126,3 +126,16 @@ type: embed
126
126
  </Figure>
127
127
  </Guidelines>
128
128
  ```
129
+
130
+ ```js
131
+ ---
132
+ type: embed
133
+ ---
134
+ <Guidelines>
135
+ <Figure recommendation="a11y" title="Accessibility">
136
+ <Figure.Item>
137
+ To indicate the current element within a breadcrumb, the <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current">aria-current</a> attribute is used. In this component, aria-current="page" will automatically be applied to the last element, and we recommend that the current page always be the last element in the breadcrumb. If the last element is not the current page, the isCurrentPage property should be applied to the relevant Breadcrumb.Link to ensure compatibility with screen readers.
138
+ </Figure.Item>
139
+ </Figure>
140
+ </Guidelines>
141
+ ```
@@ -111,4 +111,68 @@ describe('<Breadcrumb />', () => {
111
111
  expect(icon).toBeInTheDocument()
112
112
  expect(icon).toHaveAttribute('aria-hidden', 'true')
113
113
  })
114
+
115
+ it('should add aria-current="page" to the last element by default', () => {
116
+ const { container } = render(
117
+ <Breadcrumb label={TEST_LABEL}>
118
+ <Breadcrumb.Link href={TEST_LINK}>{TEST_TEXT_01}</Breadcrumb.Link>
119
+ <Breadcrumb.Link>{TEST_TEXT_02}</Breadcrumb.Link>
120
+ </Breadcrumb>
121
+ )
122
+ const links = container.querySelectorAll('[class$="--block-link"]')
123
+ const firstLink = links[0]
124
+ const lastLink = links[links.length - 1]
125
+
126
+ expect(firstLink).not.toHaveAttribute('aria-current', 'page')
127
+ expect(lastLink).toHaveAttribute('aria-current', 'page')
128
+ })
129
+
130
+ it('should add aria-current="page" to the element if isCurrent is true', () => {
131
+ const { container } = render(
132
+ <Breadcrumb label={TEST_LABEL}>
133
+ <Breadcrumb.Link isCurrentPage href={TEST_LINK}>
134
+ {TEST_TEXT_01}
135
+ </Breadcrumb.Link>
136
+ <Breadcrumb.Link>{TEST_TEXT_02}</Breadcrumb.Link>
137
+ </Breadcrumb>
138
+ )
139
+ const links = container.querySelectorAll('[class$="--block-link"]')
140
+ const firstLink = links[0]
141
+ const lastLink = links[links.length - 1]
142
+
143
+ expect(firstLink).toHaveAttribute('aria-current', 'page')
144
+ expect(lastLink).not.toHaveAttribute('aria-current', 'page')
145
+ })
146
+
147
+ it('should throw a warning when multiple elements have isCurrent set to true ', () => {
148
+ render(
149
+ <Breadcrumb label={TEST_LABEL}>
150
+ <Breadcrumb.Link isCurrentPage href={TEST_LINK}>
151
+ {TEST_TEXT_01}
152
+ </Breadcrumb.Link>
153
+ <Breadcrumb.Link isCurrentPage>{TEST_TEXT_02}</Breadcrumb.Link>
154
+ </Breadcrumb>
155
+ )
156
+
157
+ expect(consoleWarningMock).toHaveBeenCalledWith(
158
+ expect.stringContaining(
159
+ 'Warning: Multiple elements with isCurrentPage=true found. Only one element should be set to current.'
160
+ )
161
+ )
162
+ })
163
+
164
+ it('should not add aria-current="page" to the last element if it set to false', () => {
165
+ const { container } = render(
166
+ <Breadcrumb label={TEST_LABEL}>
167
+ <Breadcrumb.Link href={TEST_LINK}>{TEST_TEXT_01}</Breadcrumb.Link>
168
+ <Breadcrumb.Link isCurrentPage={false}>{TEST_TEXT_02}</Breadcrumb.Link>
169
+ </Breadcrumb>
170
+ )
171
+ const links = container.querySelectorAll('[class$="--block-link"]')
172
+ const firstLink = links[0]
173
+ const lastLink = links[links.length - 1]
174
+
175
+ expect(firstLink).not.toHaveAttribute('aria-current', 'page')
176
+ expect(lastLink).not.toHaveAttribute('aria-current', 'page')
177
+ })
114
178
  })
@@ -62,6 +62,16 @@ class Breadcrumb extends Component<BreadcrumbProps> {
62
62
  this.ref = el
63
63
  }
64
64
 
65
+ addAriaCurrent = (child: React.ReactNode) => {
66
+ const updatedChild = React.cloneElement(
67
+ child as React.ReactElement<{ 'aria-current'?: string }>,
68
+ {
69
+ 'aria-current': 'page'
70
+ }
71
+ )
72
+ return updatedChild
73
+ }
74
+
65
75
  componentDidMount() {
66
76
  this.props.makeStyles?.()
67
77
  }
@@ -77,10 +87,28 @@ class Breadcrumb extends Component<BreadcrumbProps> {
77
87
  const inlineStyle = {
78
88
  maxWidth: `${Math.floor(100 / numChildren)}%`
79
89
  }
90
+ let isAriaCurrentSet = false
91
+
80
92
  return React.Children.map(children, (child, index) => {
93
+ const isLastElement = index === numChildren - 1
94
+ if (React.isValidElement(child)) {
95
+ const isCurrentPage = child.props.isCurrentPage || false
96
+ if (isAriaCurrentSet && isCurrentPage) {
97
+ console.warn(
98
+ `Warning: Multiple elements with isCurrentPage=true found. Only one element should be set to current.`
99
+ )
100
+ }
101
+ if (isCurrentPage) {
102
+ isAriaCurrentSet = true
103
+ }
104
+ }
81
105
  return (
82
106
  <li css={styles?.crumb} style={inlineStyle}>
83
- {child}
107
+ {!isAriaCurrentSet &&
108
+ isLastElement &&
109
+ (child as React.ReactElement).props.isCurrentPage !== false
110
+ ? this.addAriaCurrent(child)
111
+ : child}
84
112
  {index < numChildren - 1 && (
85
113
  <IconArrowOpenEndSolid color="auto" css={styles?.separator} />
86
114
  )}