@instructure/ui-options 8.19.1-snapshot.8 → 8.20.0

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,6 +3,13 @@
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
+ # [8.20.0](https://github.com/instructure/instructure-ui/compare/v8.19.0...v8.20.0) (2022-03-22)
7
+
8
+ ### Features
9
+
10
+ - **ui-options:** add `href` prop to Options.Item ([49bf28c](https://github.com/instructure/instructure-ui/commit/49bf28c637f395f7ae5d23c0f676d83a15c2ee43))
11
+ - **ui-options:** modify default nested Options padding to have no extra padding ([bfc4f7c](https://github.com/instructure/instructure-ui/commit/bfc4f7cb094d91d25b58a87a24a473363c90796b))
12
+
6
13
  # [8.19.0](https://github.com/instructure/instructure-ui/compare/v8.18.0...v8.19.0) (2022-03-16)
7
14
 
8
15
  ### Bug Fixes
@@ -27,6 +27,7 @@ export default {
27
27
  propValues: {
28
28
  renderAfterLabel: [null, IconArrowOpenEndSolid],
29
29
  renderBeforeLabel: [null, IconCheckSolid],
30
+ href: [void 0, '/hello'],
30
31
  description: [null, 'Curabitur fringilla, urna ut efficitur molestie, nibh lacus tincidunt elit, ut tempor ipsum nunc sit amet massa. Integer sit amet ante vitae lectus gravida pulvinar.']
31
32
  },
32
33
  getComponentProps: props => {
@@ -60,6 +61,10 @@ export default {
60
61
  }
61
62
  }
62
63
 
64
+ if (props.href && (props.beforeLabelContentVAlign !== 'center' || props.afterLabelContentVAlign !== 'center' || props.description)) {
65
+ return true;
66
+ }
67
+
63
68
  return false;
64
69
  }
65
70
  };
@@ -1,4 +1,4 @@
1
- var _dec, _dec2, _class, _class2, _temp;
1
+ var _dec, _dec2, _dec3, _class, _class2, _temp;
2
2
 
3
3
  /*
4
4
  * The MIT License (MIT)
@@ -26,7 +26,7 @@ var _dec, _dec2, _class, _class2, _temp;
26
26
 
27
27
  /** @jsx jsx */
28
28
  import { Component } from 'react';
29
- import { omitProps, getElementType, callRenderProp } from '@instructure/ui-react-utils';
29
+ import { omitProps, getElementType, callRenderProp, withDeterministicId } from '@instructure/ui-react-utils';
30
30
  import { testable } from '@instructure/ui-testable';
31
31
  import { withStyle, jsx } from '@instructure/emotion';
32
32
  import generateStyles from './styles';
@@ -40,10 +40,12 @@ id: Options.Item
40
40
  @tsProps
41
41
  **/
42
42
 
43
- let Item = (_dec = withStyle(generateStyles, generateComponentTheme), _dec2 = testable(), _dec(_class = _dec2(_class = (_temp = _class2 = class Item extends Component {
44
- constructor() {
45
- super(...arguments);
43
+ let Item = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyles, generateComponentTheme), _dec3 = testable(), _dec(_class = _dec2(_class = _dec3(_class = (_temp = _class2 = class Item extends Component {
44
+ constructor(props) {
45
+ super(props);
46
46
  this.ref = null;
47
+ this._descriptionId = void 0;
48
+ this._descriptionId = props.deterministicId('OptionsItem-description');
47
49
  }
48
50
 
49
51
  componentDidMount() {
@@ -80,6 +82,7 @@ let Item = (_dec = withStyle(generateStyles, generateComponentTheme), _dec2 = te
80
82
  render() {
81
83
  const _this$props4 = this.props,
82
84
  as = _this$props4.as,
85
+ href = _this$props4.href,
83
86
  role = _this$props4.role,
84
87
  styles = _this$props4.styles,
85
88
  description = _this$props4.description,
@@ -89,6 +92,7 @@ let Item = (_dec = withStyle(generateStyles, generateComponentTheme), _dec2 = te
89
92
  elementRef = _this$props4.elementRef,
90
93
  children = _this$props4.children;
91
94
  const ElementType = getElementType(Item, this.props, () => as);
95
+ const InnerElementType = href ? 'a' : 'span';
92
96
  const passthroughProps = omitProps(this.props, Item.allowedProps);
93
97
  const childrenContent = callRenderProp(children);
94
98
  const descriptionContent = callRenderProp(description);
@@ -102,12 +106,15 @@ let Item = (_dec = withStyle(generateStyles, generateComponentTheme), _dec2 = te
102
106
  elementRef(element);
103
107
  }
104
108
  }
105
- }, jsx("span", Object.assign({}, passthroughProps, {
109
+ }, jsx(InnerElementType, Object.assign({}, passthroughProps, {
106
110
  css: styles === null || styles === void 0 ? void 0 : styles.container,
107
- role: role
111
+ role: href ? void 0 : role,
112
+ href: href,
113
+ "aria-describedby": this.props['aria-describedby'] || (descriptionContent ? this._descriptionId : void 0)
108
114
  }), childrenContent, descriptionContent && jsx("span", {
109
115
  css: styles === null || styles === void 0 ? void 0 : styles.description,
110
- role: descriptionRole
116
+ role: descriptionRole,
117
+ id: this._descriptionId
111
118
  }, descriptionContent)), renderBeforeLabel && this.renderContent(renderBeforeLabel, styles === null || styles === void 0 ? void 0 : styles.contentBefore), renderAfterLabel && this.renderContent(renderAfterLabel, styles === null || styles === void 0 ? void 0 : styles.contentAfter));
112
119
  }
113
120
 
@@ -118,6 +125,6 @@ let Item = (_dec = withStyle(generateStyles, generateComponentTheme), _dec2 = te
118
125
  descriptionRole: 'note',
119
126
  beforeLabelContentVAlign: 'center',
120
127
  afterLabelContentVAlign: 'center'
121
- }, _temp)) || _class) || _class);
128
+ }, _temp)) || _class) || _class) || _class);
122
129
  export default Item;
123
130
  export { Item };
@@ -24,7 +24,7 @@
24
24
  import PropTypes from 'prop-types';
25
25
  const propTypes = {
26
26
  as: PropTypes.elementType,
27
- variant: PropTypes.oneOf(['default', 'highlighted', 'selected', 'disabled']),
27
+ variant: PropTypes.oneOf(['default', 'highlighted', 'selected', 'disabled', 'highlighted-disabled']),
28
28
  role: PropTypes.string,
29
29
  renderBeforeLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
30
30
  renderAfterLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
@@ -32,8 +32,9 @@ const propTypes = {
32
32
  afterLabelContentVAlign: PropTypes.oneOf(['start', 'center', 'end']),
33
33
  description: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
34
34
  descriptionRole: PropTypes.string,
35
+ href: PropTypes.string,
35
36
  elementRef: PropTypes.func,
36
37
  children: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
37
38
  };
38
- const allowedProps = ['as', 'variant', 'role', 'renderBeforeLabel', 'renderAfterLabel', 'beforeLabelContentVAlign', 'afterLabelContentVAlign', 'description', 'descriptionRole', 'elementRef', 'children'];
39
+ const allowedProps = ['as', 'variant', 'role', 'renderBeforeLabel', 'renderAfterLabel', 'beforeLabelContentVAlign', 'afterLabelContentVAlign', 'description', 'descriptionRole', 'href', 'elementRef', 'children'];
39
40
  export { propTypes, allowedProps };
@@ -55,6 +55,12 @@ const generateStyle = (componentTheme, props) => {
55
55
  cursor: 'not-allowed',
56
56
  opacity: 0.5
57
57
  },
58
+ ['highlighted-disabled']: {
59
+ background: componentTheme.highlightedBackground,
60
+ color: componentTheme.highlightedLabelColor,
61
+ cursor: 'not-allowed',
62
+ opacity: 0.5
63
+ },
58
64
  default: {}
59
65
  };
60
66
 
@@ -78,6 +84,10 @@ const generateStyle = (componentTheme, props) => {
78
84
  }[vAlign];
79
85
  };
80
86
 
87
+ const linkStyles = {
88
+ textDecoration: 'none',
89
+ color: 'currentColor'
90
+ };
81
91
  const transition = 'background 200ms';
82
92
  return {
83
93
  item: {
@@ -129,7 +139,14 @@ const generateStyle = (componentTheme, props) => {
129
139
  ...(hasContentBeforeLabel && hasContentAfterLabel && {
130
140
  paddingInlineEnd: `calc(${componentTheme.iconPadding} * 2 + 1em)`,
131
141
  paddingInlineStart: `calc(${componentTheme.iconPadding} * 2 + 1em)`
132
- })
142
+ }),
143
+ // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
144
+ '&:is(a)': {
145
+ '&, &:link, &:visited, &:active, &:hover, &:focus': linkStyles
146
+ },
147
+ '&:-webkit-any(a)': {
148
+ '&, &:link, &:visited, &:active, &:hover, &:focus': linkStyles
149
+ }
133
150
  },
134
151
  content: {
135
152
  label: 'optionItem__content',
@@ -51,7 +51,7 @@ const generateComponentTheme = theme => {
51
51
  selectedBackground: colors === null || colors === void 0 ? void 0 : colors.backgroundDark,
52
52
  padding: `${spacing === null || spacing === void 0 ? void 0 : spacing.xSmall} ${spacing === null || spacing === void 0 ? void 0 : spacing.small}`,
53
53
  iconPadding: spacing === null || spacing === void 0 ? void 0 : spacing.small,
54
- nestedPadding: spacing === null || spacing === void 0 ? void 0 : spacing.medium,
54
+ nestedPadding: spacing === null || spacing === void 0 ? void 0 : spacing.small,
55
55
  beforeLabelContentVOffset: '0.625rem',
56
56
  afterLabelContentVOffset: '0.625rem',
57
57
  descriptionFontSize: typography.fontSizeSmall,
@@ -106,7 +106,7 @@ let Options = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyles, g
106
106
 
107
107
  if (matchComponentTypes(child, ['Item', 'Separator'])) {
108
108
  return safeCloneElement(child, {
109
- as: this.childAs
109
+ as: this.childAs || child.props.as
110
110
  });
111
111
  }
112
112
 
@@ -135,7 +135,7 @@ let Options = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyles, g
135
135
  margin: "none",
136
136
  padding: "none",
137
137
  background: "primary",
138
- "aria-labelledby": renderLabel ? this._labelId : void 0
138
+ "aria-labelledby": renderLabel ? this._labelId : this.props['aria-labelledby'] ? this.props['aria-labelledby'] : void 0
139
139
  }), this.renderChildren()));
140
140
  }
141
141
 
@@ -35,6 +35,7 @@ var _default = {
35
35
  propValues: {
36
36
  renderAfterLabel: [null, _uiIcons.IconArrowOpenEndSolid],
37
37
  renderBeforeLabel: [null, _uiIcons.IconCheckSolid],
38
+ href: [void 0, '/hello'],
38
39
  description: [null, 'Curabitur fringilla, urna ut efficitur molestie, nibh lacus tincidunt elit, ut tempor ipsum nunc sit amet massa. Integer sit amet ante vitae lectus gravida pulvinar.']
39
40
  },
40
41
  getComponentProps: props => {
@@ -68,6 +69,10 @@ var _default = {
68
69
  }
69
70
  }
70
71
 
72
+ if (props.href && (props.beforeLabelContentVAlign !== 'center' || props.afterLabelContentVAlign !== 'center' || props.description)) {
73
+ return true;
74
+ }
75
+
71
76
  return false;
72
77
  }
73
78
  };
@@ -21,7 +21,7 @@ var _theme = _interopRequireDefault(require("./theme"));
21
21
 
22
22
  var _props = require("./props");
23
23
 
24
- var _dec, _dec2, _class, _class2, _temp;
24
+ var _dec, _dec2, _dec3, _class, _class2, _temp;
25
25
 
26
26
  /**
27
27
  ---
@@ -30,10 +30,12 @@ id: Options.Item
30
30
  ---
31
31
  @tsProps
32
32
  **/
33
- let Item = (_dec = (0, _emotion.withStyle)(_styles.default, _theme.default), _dec2 = (0, _uiTestable.testable)(), _dec(_class = _dec2(_class = (_temp = _class2 = class Item extends _react.Component {
34
- constructor() {
35
- super(...arguments);
33
+ let Item = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec2 = (0, _emotion.withStyle)(_styles.default, _theme.default), _dec3 = (0, _uiTestable.testable)(), _dec(_class = _dec2(_class = _dec3(_class = (_temp = _class2 = class Item extends _react.Component {
34
+ constructor(props) {
35
+ super(props);
36
36
  this.ref = null;
37
+ this._descriptionId = void 0;
38
+ this._descriptionId = props.deterministicId('OptionsItem-description');
37
39
  }
38
40
 
39
41
  componentDidMount() {
@@ -70,6 +72,7 @@ let Item = (_dec = (0, _emotion.withStyle)(_styles.default, _theme.default), _de
70
72
  render() {
71
73
  const _this$props4 = this.props,
72
74
  as = _this$props4.as,
75
+ href = _this$props4.href,
73
76
  role = _this$props4.role,
74
77
  styles = _this$props4.styles,
75
78
  description = _this$props4.description,
@@ -79,6 +82,7 @@ let Item = (_dec = (0, _emotion.withStyle)(_styles.default, _theme.default), _de
79
82
  elementRef = _this$props4.elementRef,
80
83
  children = _this$props4.children;
81
84
  const ElementType = (0, _uiReactUtils.getElementType)(Item, this.props, () => as);
85
+ const InnerElementType = href ? 'a' : 'span';
82
86
  const passthroughProps = (0, _uiReactUtils.omitProps)(this.props, Item.allowedProps);
83
87
  const childrenContent = (0, _uiReactUtils.callRenderProp)(children);
84
88
  const descriptionContent = (0, _uiReactUtils.callRenderProp)(description);
@@ -92,12 +96,15 @@ let Item = (_dec = (0, _emotion.withStyle)(_styles.default, _theme.default), _de
92
96
  elementRef(element);
93
97
  }
94
98
  }
95
- }, (0, _emotion.jsx)("span", Object.assign({}, passthroughProps, {
99
+ }, (0, _emotion.jsx)(InnerElementType, Object.assign({}, passthroughProps, {
96
100
  css: styles === null || styles === void 0 ? void 0 : styles.container,
97
- role: role
101
+ role: href ? void 0 : role,
102
+ href: href,
103
+ "aria-describedby": this.props['aria-describedby'] || (descriptionContent ? this._descriptionId : void 0)
98
104
  }), childrenContent, descriptionContent && (0, _emotion.jsx)("span", {
99
105
  css: styles === null || styles === void 0 ? void 0 : styles.description,
100
- role: descriptionRole
106
+ role: descriptionRole,
107
+ id: this._descriptionId
101
108
  }, descriptionContent)), renderBeforeLabel && this.renderContent(renderBeforeLabel, styles === null || styles === void 0 ? void 0 : styles.contentBefore), renderAfterLabel && this.renderContent(renderAfterLabel, styles === null || styles === void 0 ? void 0 : styles.contentAfter));
102
109
  }
103
110
 
@@ -108,7 +115,7 @@ let Item = (_dec = (0, _emotion.withStyle)(_styles.default, _theme.default), _de
108
115
  descriptionRole: 'note',
109
116
  beforeLabelContentVAlign: 'center',
110
117
  afterLabelContentVAlign: 'center'
111
- }, _temp)) || _class) || _class);
118
+ }, _temp)) || _class) || _class) || _class);
112
119
  exports.Item = Item;
113
120
  var _default = Item;
114
121
  exports.default = _default;
@@ -34,7 +34,7 @@ var _propTypes = _interopRequireDefault(require("prop-types"));
34
34
  */
35
35
  const propTypes = {
36
36
  as: _propTypes.default.elementType,
37
- variant: _propTypes.default.oneOf(['default', 'highlighted', 'selected', 'disabled']),
37
+ variant: _propTypes.default.oneOf(['default', 'highlighted', 'selected', 'disabled', 'highlighted-disabled']),
38
38
  role: _propTypes.default.string,
39
39
  renderBeforeLabel: _propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.func]),
40
40
  renderAfterLabel: _propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.func]),
@@ -42,9 +42,10 @@ const propTypes = {
42
42
  afterLabelContentVAlign: _propTypes.default.oneOf(['start', 'center', 'end']),
43
43
  description: _propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.func]),
44
44
  descriptionRole: _propTypes.default.string,
45
+ href: _propTypes.default.string,
45
46
  elementRef: _propTypes.default.func,
46
47
  children: _propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.func])
47
48
  };
48
49
  exports.propTypes = propTypes;
49
- const allowedProps = ['as', 'variant', 'role', 'renderBeforeLabel', 'renderAfterLabel', 'beforeLabelContentVAlign', 'afterLabelContentVAlign', 'description', 'descriptionRole', 'elementRef', 'children'];
50
+ const allowedProps = ['as', 'variant', 'role', 'renderBeforeLabel', 'renderAfterLabel', 'beforeLabelContentVAlign', 'afterLabelContentVAlign', 'description', 'descriptionRole', 'href', 'elementRef', 'children'];
50
51
  exports.allowedProps = allowedProps;
@@ -63,6 +63,12 @@ const generateStyle = (componentTheme, props) => {
63
63
  cursor: 'not-allowed',
64
64
  opacity: 0.5
65
65
  },
66
+ ['highlighted-disabled']: {
67
+ background: componentTheme.highlightedBackground,
68
+ color: componentTheme.highlightedLabelColor,
69
+ cursor: 'not-allowed',
70
+ opacity: 0.5
71
+ },
66
72
  default: {}
67
73
  };
68
74
 
@@ -86,6 +92,10 @@ const generateStyle = (componentTheme, props) => {
86
92
  }[vAlign];
87
93
  };
88
94
 
95
+ const linkStyles = {
96
+ textDecoration: 'none',
97
+ color: 'currentColor'
98
+ };
89
99
  const transition = 'background 200ms';
90
100
  return {
91
101
  item: {
@@ -137,7 +147,14 @@ const generateStyle = (componentTheme, props) => {
137
147
  ...(hasContentBeforeLabel && hasContentAfterLabel && {
138
148
  paddingInlineEnd: `calc(${componentTheme.iconPadding} * 2 + 1em)`,
139
149
  paddingInlineStart: `calc(${componentTheme.iconPadding} * 2 + 1em)`
140
- })
150
+ }),
151
+ // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
152
+ '&:is(a)': {
153
+ '&, &:link, &:visited, &:active, &:hover, &:focus': linkStyles
154
+ },
155
+ '&:-webkit-any(a)': {
156
+ '&, &:link, &:visited, &:active, &:hover, &:focus': linkStyles
157
+ }
141
158
  },
142
159
  content: {
143
160
  label: 'optionItem__content',
@@ -58,7 +58,7 @@ const generateComponentTheme = theme => {
58
58
  selectedBackground: colors === null || colors === void 0 ? void 0 : colors.backgroundDark,
59
59
  padding: `${spacing === null || spacing === void 0 ? void 0 : spacing.xSmall} ${spacing === null || spacing === void 0 ? void 0 : spacing.small}`,
60
60
  iconPadding: spacing === null || spacing === void 0 ? void 0 : spacing.small,
61
- nestedPadding: spacing === null || spacing === void 0 ? void 0 : spacing.medium,
61
+ nestedPadding: spacing === null || spacing === void 0 ? void 0 : spacing.small,
62
62
  beforeLabelContentVOffset: '0.625rem',
63
63
  afterLabelContentVOffset: '0.625rem',
64
64
  descriptionFontSize: typography.fontSizeSmall,
@@ -101,7 +101,7 @@ let Options = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec2 = (0, _emo
101
101
 
102
102
  if ((0, _uiReactUtils.matchComponentTypes)(child, ['Item', 'Separator'])) {
103
103
  return (0, _uiReactUtils.safeCloneElement)(child, {
104
- as: this.childAs
104
+ as: this.childAs || child.props.as
105
105
  });
106
106
  }
107
107
 
@@ -131,7 +131,7 @@ let Options = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec2 = (0, _emo
131
131
  margin: "none",
132
132
  padding: "none",
133
133
  background: "primary",
134
- "aria-labelledby": renderLabel ? this._labelId : void 0
134
+ "aria-labelledby": renderLabel ? this._labelId : this.props['aria-labelledby'] ? this.props['aria-labelledby'] : void 0
135
135
  }), this.renderChildren()));
136
136
  }
137
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-options",
3
- "version": "8.19.1-snapshot.8+8648faa3b",
3
+ "version": "8.20.0",
4
4
  "description": "A view-only component for composing interactive lists and menus.",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -23,21 +23,21 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "devDependencies": {
26
- "@instructure/ui-babel-preset": "8.19.1-snapshot.8+8648faa3b",
27
- "@instructure/ui-color-utils": "8.19.1-snapshot.8+8648faa3b",
28
- "@instructure/ui-test-locator": "8.19.1-snapshot.8+8648faa3b",
29
- "@instructure/ui-test-utils": "8.19.1-snapshot.8+8648faa3b",
30
- "@instructure/ui-themes": "8.19.1-snapshot.8+8648faa3b"
26
+ "@instructure/ui-babel-preset": "8.20.0",
27
+ "@instructure/ui-color-utils": "8.20.0",
28
+ "@instructure/ui-test-locator": "8.20.0",
29
+ "@instructure/ui-test-utils": "8.20.0",
30
+ "@instructure/ui-themes": "8.20.0"
31
31
  },
32
32
  "dependencies": {
33
33
  "@babel/runtime": "^7.13.10",
34
- "@instructure/emotion": "8.19.1-snapshot.8+8648faa3b",
35
- "@instructure/shared-types": "8.19.1-snapshot.8+8648faa3b",
36
- "@instructure/ui-icons": "8.19.1-snapshot.8+8648faa3b",
37
- "@instructure/ui-prop-types": "8.19.1-snapshot.8+8648faa3b",
38
- "@instructure/ui-react-utils": "8.19.1-snapshot.8+8648faa3b",
39
- "@instructure/ui-testable": "8.19.1-snapshot.8+8648faa3b",
40
- "@instructure/ui-view": "8.19.1-snapshot.8+8648faa3b",
34
+ "@instructure/emotion": "8.20.0",
35
+ "@instructure/shared-types": "8.20.0",
36
+ "@instructure/ui-icons": "8.20.0",
37
+ "@instructure/ui-prop-types": "8.20.0",
38
+ "@instructure/ui-react-utils": "8.20.0",
39
+ "@instructure/ui-testable": "8.20.0",
40
+ "@instructure/ui-view": "8.20.0",
41
41
  "prop-types": "^15"
42
42
  },
43
43
  "peerDependencies": {
@@ -46,6 +46,5 @@
46
46
  "publishConfig": {
47
47
  "access": "public"
48
48
  },
49
- "sideEffects": false,
50
- "gitHead": "8648faa3b9f006a729a4d13195dd9c28e23d1335"
49
+ "sideEffects": false
51
50
  }
@@ -31,6 +31,7 @@ export default {
31
31
  propValues: {
32
32
  renderAfterLabel: [null, IconArrowOpenEndSolid],
33
33
  renderBeforeLabel: [null, IconCheckSolid],
34
+ href: [undefined, '/hello'],
34
35
  description: [
35
36
  null,
36
37
  'Curabitur fringilla, urna ut efficitur molestie, nibh lacus tincidunt elit, ut tempor ipsum nunc sit amet massa. Integer sit amet ante vitae lectus gravida pulvinar.'
@@ -76,6 +77,15 @@ export default {
76
77
  }
77
78
  }
78
79
 
80
+ if (
81
+ props.href &&
82
+ (props.beforeLabelContentVAlign !== 'center' ||
83
+ props.afterLabelContentVAlign !== 'center' ||
84
+ props.description)
85
+ ) {
86
+ return true
87
+ }
88
+
79
89
  return false
80
90
  }
81
91
  } as StoryConfig<OptionsItemProps>
@@ -28,7 +28,8 @@ import { Component } from 'react'
28
28
  import {
29
29
  omitProps,
30
30
  getElementType,
31
- callRenderProp
31
+ callRenderProp,
32
+ withDeterministicId
32
33
  } from '@instructure/ui-react-utils'
33
34
  import { testable } from '@instructure/ui-testable'
34
35
 
@@ -46,6 +47,7 @@ id: Options.Item
46
47
  ---
47
48
  @tsProps
48
49
  **/
50
+ @withDeterministicId()
49
51
  @withStyle(generateStyles, generateComponentTheme)
50
52
  @testable()
51
53
  class Item extends Component<OptionsItemProps> {
@@ -65,6 +67,14 @@ class Item extends Component<OptionsItemProps> {
65
67
 
66
68
  ref: Element | null = null
67
69
 
70
+ private readonly _descriptionId: string
71
+
72
+ constructor(props: OptionsItemProps) {
73
+ super(props)
74
+
75
+ this._descriptionId = props.deterministicId!('OptionsItem-description')
76
+ }
77
+
68
78
  componentDidMount() {
69
79
  this.props.makeStyles?.()
70
80
  }
@@ -102,6 +112,7 @@ class Item extends Component<OptionsItemProps> {
102
112
  render() {
103
113
  const {
104
114
  as,
115
+ href,
105
116
  role,
106
117
  styles,
107
118
  description,
@@ -113,6 +124,9 @@ class Item extends Component<OptionsItemProps> {
113
124
  } = this.props
114
125
 
115
126
  const ElementType = getElementType(Item, this.props, () => as!)
127
+
128
+ const InnerElementType = href ? 'a' : 'span'
129
+
116
130
  const passthroughProps = omitProps(this.props, Item.allowedProps)
117
131
 
118
132
  const childrenContent = callRenderProp(children)
@@ -131,14 +145,27 @@ class Item extends Component<OptionsItemProps> {
131
145
  }
132
146
  }}
133
147
  >
134
- <span {...passthroughProps} css={styles?.container} role={role}>
148
+ <InnerElementType
149
+ {...passthroughProps}
150
+ css={styles?.container}
151
+ role={href ? undefined : role}
152
+ href={href}
153
+ aria-describedby={
154
+ this.props['aria-describedby'] ||
155
+ (descriptionContent ? this._descriptionId : undefined)
156
+ }
157
+ >
135
158
  {childrenContent}
136
159
  {descriptionContent && (
137
- <span css={styles?.description} role={descriptionRole}>
160
+ <span
161
+ css={styles?.description}
162
+ role={descriptionRole}
163
+ id={this._descriptionId}
164
+ >
138
165
  {descriptionContent}
139
166
  </span>
140
167
  )}
141
- </span>
168
+ </InnerElementType>
142
169
  {renderBeforeLabel &&
143
170
  this.renderContent(renderBeforeLabel, styles?.contentBefore)}
144
171
  {renderAfterLabel &&
@@ -31,18 +31,24 @@ import type {
31
31
  OptionsItemTheme,
32
32
  OtherHTMLAttributes
33
33
  } from '@instructure/shared-types'
34
+ import type { WithDeterministicIdProps } from '@instructure/ui-react-utils'
34
35
  import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
35
36
 
36
37
  type ItemProps = {
37
38
  children?: React.ReactNode | (() => React.ReactNode)
38
39
  /**
39
- * Element type to render as
40
+ * Element type to render as. Will be set to `<a>` if href is provided.
40
41
  */
41
42
  as?: AsElementType
42
43
  /**
43
44
  * The style variant of the item
44
45
  */
45
- variant?: 'default' | 'highlighted' | 'selected' | 'disabled'
46
+ variant?:
47
+ | 'default'
48
+ | 'highlighted'
49
+ | 'selected'
50
+ | 'disabled'
51
+ | 'highlighted-disabled'
46
52
  /**
47
53
  * The ARIA role of the element
48
54
  */
@@ -76,6 +82,10 @@ type OptionsItemOwnProps = ItemProps & {
76
82
  * The ARIA role of the description element
77
83
  */
78
84
  descriptionRole?: string
85
+ /**
86
+ * Providing href will render the option as `<a>`.
87
+ */
88
+ href?: string
79
89
  /**
80
90
  * provides a reference to the underlying html root element
81
91
  */
@@ -88,6 +98,7 @@ type AllowedPropKeys = Readonly<Array<PropKeys>>
88
98
 
89
99
  type OptionsItemProps = OptionsItemOwnProps &
90
100
  WithStyleProps<OptionsItemTheme, OptionsItemStyle> &
101
+ WithDeterministicIdProps &
91
102
  OtherHTMLAttributes<OptionsItemOwnProps>
92
103
 
93
104
  type OptionsItemStyle = ComponentStyle<
@@ -101,7 +112,13 @@ type OptionsItemStyle = ComponentStyle<
101
112
 
102
113
  const propTypes: PropValidators<PropKeys> = {
103
114
  as: PropTypes.elementType,
104
- variant: PropTypes.oneOf(['default', 'highlighted', 'selected', 'disabled']),
115
+ variant: PropTypes.oneOf([
116
+ 'default',
117
+ 'highlighted',
118
+ 'selected',
119
+ 'disabled',
120
+ 'highlighted-disabled'
121
+ ]),
105
122
  role: PropTypes.string,
106
123
  renderBeforeLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
107
124
  renderAfterLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
@@ -109,6 +126,7 @@ const propTypes: PropValidators<PropKeys> = {
109
126
  afterLabelContentVAlign: PropTypes.oneOf(['start', 'center', 'end']),
110
127
  description: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
111
128
  descriptionRole: PropTypes.string,
129
+ href: PropTypes.string,
112
130
  elementRef: PropTypes.func,
113
131
  children: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
114
132
  }
@@ -123,6 +141,7 @@ const allowedProps: AllowedPropKeys = [
123
141
  'afterLabelContentVAlign',
124
142
  'description',
125
143
  'descriptionRole',
144
+ 'href',
126
145
  'elementRef',
127
146
  'children'
128
147
  ]
@@ -63,6 +63,12 @@ const generateStyle = (
63
63
  color: componentTheme.highlightedLabelColor
64
64
  },
65
65
  disabled: { cursor: 'not-allowed', opacity: 0.5 },
66
+ ['highlighted-disabled']: {
67
+ background: componentTheme.highlightedBackground,
68
+ color: componentTheme.highlightedLabelColor,
69
+ cursor: 'not-allowed',
70
+ opacity: 0.5
71
+ },
66
72
  default: {}
67
73
  }
68
74
 
@@ -92,6 +98,8 @@ const generateStyle = (
92
98
  }[vAlign!]
93
99
  }
94
100
 
101
+ const linkStyles = { textDecoration: 'none', color: 'currentColor' }
102
+
95
103
  const transition = 'background 200ms'
96
104
 
97
105
  return {
@@ -143,7 +151,15 @@ const generateStyle = (
143
151
  hasContentAfterLabel && {
144
152
  paddingInlineEnd: `calc(${componentTheme.iconPadding} * 2 + 1em)`,
145
153
  paddingInlineStart: `calc(${componentTheme.iconPadding} * 2 + 1em)`
146
- })
154
+ }),
155
+
156
+ // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
157
+ '&:is(a)': {
158
+ '&, &:link, &:visited, &:active, &:hover, &:focus': linkStyles
159
+ },
160
+ '&:-webkit-any(a)': {
161
+ '&, &:link, &:visited, &:active, &:hover, &:focus': linkStyles
162
+ }
147
163
  },
148
164
  content: {
149
165
  label: 'optionItem__content',
@@ -55,7 +55,7 @@ const generateComponentTheme = (theme: Theme): OptionsItemTheme => {
55
55
 
56
56
  padding: `${spacing?.xSmall} ${spacing?.small}`,
57
57
  iconPadding: spacing?.small,
58
- nestedPadding: spacing?.medium,
58
+ nestedPadding: spacing?.small,
59
59
 
60
60
  beforeLabelContentVOffset: '0.625rem',
61
61
  afterLabelContentVOffset: '0.625rem',