@astryxdesign/core 0.4.2 → 0.4.3-canary.5939961
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 +45 -0
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +3 -19
- package/dist/Banner/Banner.d.ts +6 -2
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +71 -30
- package/dist/Banner/index.d.ts +14 -12
- package/dist/Banner/index.d.ts.map +1 -1
- package/dist/Banner/index.js +10 -8
- package/dist/Chat/ChatTokenizedText.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +96 -35
- package/dist/ComplexSelector/index.d.ts +2 -2
- package/dist/ComplexSelector/index.d.ts.map +1 -1
- package/dist/ComplexSelector/index.js +1 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +55 -12
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +4 -1
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +2 -4
- package/dist/Table/columnUtils.d.ts.map +1 -1
- package/dist/Table/columnUtils.js +4 -1
- package/dist/TextArea/TextArea.d.ts +6 -3
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +17 -6
- package/dist/TreeList/TreeList.js +2 -1
- package/dist/astryx.css +6 -4
- package/dist/astryx.umd.js +51 -51
- package/dist/astryx.umd.js.map +4 -4
- package/dist/theme/defineTheme.d.ts +30 -7
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +36 -47
- package/dist/theme/expandColorScale.d.ts +17 -2
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +63 -33
- package/dist/theme/mergeComponents.d.ts +20 -0
- package/dist/theme/mergeComponents.d.ts.map +1 -0
- package/dist/theme/mergeComponents.js +56 -0
- package/dist/theme/onMediaTokens.d.ts +6 -1
- package/dist/theme/onMediaTokens.d.ts.map +1 -1
- package/dist/theme/onMediaTokens.js +11 -3
- package/dist/utils/characters.d.ts +27 -0
- package/dist/utils/characters.d.ts.map +1 -0
- package/dist/utils/characters.js +83 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/package.json +3 -3
- package/src/Avatar/Avatar.test.tsx +6 -1
- package/src/Avatar/Avatar.tsx +3 -21
- package/src/Banner/Banner.doc.mjs +9 -7
- package/src/Banner/Banner.test.tsx +68 -0
- package/src/Banner/Banner.tsx +97 -36
- package/src/Banner/index.ts +15 -13
- package/src/Chat/ChatTokenizedText.tsx +1 -1
- package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
- package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
- package/src/ComplexSelector/ComplexSelector.tsx +153 -32
- package/src/ComplexSelector/index.ts +3 -1
- package/src/Markdown/parser.test.ts +53 -0
- package/src/Markdown/parser.ts +53 -12
- package/src/PowerSearch/PowerSearch.test.tsx +48 -3
- package/src/PowerSearch/PowerSearch.tsx +4 -1
- package/src/PowerSearch/formatFilterValue.test.ts +22 -0
- package/src/PowerSearch/formatFilterValue.ts +2 -4
- package/src/Table/Table.test.tsx +6 -0
- package/src/Table/columnUtils.ts +3 -1
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextArea/TextArea.test.tsx +72 -0
- package/src/TextArea/TextArea.tsx +26 -8
- package/src/TreeList/TreeList.doc.mjs +2 -2
- package/src/TreeList/TreeList.tsx +1 -1
- package/src/theme/defineTheme.test.ts +127 -0
- package/src/theme/defineTheme.ts +70 -55
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/theme/extensibleAxes.test.ts +365 -0
- package/src/theme/mergeComponents.ts +59 -0
- package/src/theme/onMediaTokens.ts +9 -2
- package/src/utils/characters.test.ts +141 -0
- package/src/utils/characters.ts +88 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file characters.ts
|
|
5
|
+
* @input Strings that may contain characters made of several code units
|
|
6
|
+
* (emoji, flags, joined emoji sequences, accented letters)
|
|
7
|
+
* @output Exports characterCount, firstCharacter, truncateCharacters —
|
|
8
|
+
* replacements for .length / .charAt(0) / .slice(0, n) that measure
|
|
9
|
+
* and cut user-visible strings the way a person reads them
|
|
10
|
+
* @position Shared utility; consumed by Avatar, TextArea, PowerSearch, Table
|
|
11
|
+
*
|
|
12
|
+
* SYNC: When modified, update:
|
|
13
|
+
* - /packages/core/src/utils/characters.test.ts
|
|
14
|
+
* - /packages/core/src/utils/index.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reuse a single segmenter when the runtime supports Intl.Segmenter.
|
|
19
|
+
* `grapheme` is the Intl granularity name for a user-perceived character.
|
|
20
|
+
*/
|
|
21
|
+
const characterSegmenter = typeof Intl.Segmenter === 'function' ? new Intl.Segmenter(undefined, {
|
|
22
|
+
granularity: 'grapheme'
|
|
23
|
+
}) : null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Split a string into user-perceived characters. The code-point fallback
|
|
27
|
+
* keeps surrogate pairs intact but may split joined emoji sequences and flag
|
|
28
|
+
* pairs on runtimes without Intl.Segmenter.
|
|
29
|
+
*/
|
|
30
|
+
function splitCharacters(str) {
|
|
31
|
+
if (characterSegmenter) {
|
|
32
|
+
return [...characterSegmenter.segment(str)].map(s => s.segment);
|
|
33
|
+
}
|
|
34
|
+
return [...str];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Number of characters in a string — the count a person would give, where one
|
|
39
|
+
* emoji, flag, or accented letter is one character. Replacement for `.length`
|
|
40
|
+
* on user-visible strings, which counts internal storage units instead.
|
|
41
|
+
*/
|
|
42
|
+
export function characterCount(str) {
|
|
43
|
+
if (str === '') {
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
return splitCharacters(str).length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The first character of a string, or '' when empty. Replacement for
|
|
51
|
+
* `.charAt(0)`, which can return half of an emoji. Reads only the first
|
|
52
|
+
* character, so cost does not scale with the length of the string.
|
|
53
|
+
*/
|
|
54
|
+
export function firstCharacter(str) {
|
|
55
|
+
if (characterSegmenter) {
|
|
56
|
+
const first = characterSegmenter.segment(str)[Symbol.iterator]().next();
|
|
57
|
+
return first.done ? '' : first.value.segment;
|
|
58
|
+
}
|
|
59
|
+
const codePoint = str.codePointAt(0);
|
|
60
|
+
return codePoint == null ? '' : String.fromCodePoint(codePoint);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Truncate to at most `max` characters, ellipsis included: strings within
|
|
65
|
+
* `max` characters pass through unchanged; longer strings are cut so the
|
|
66
|
+
* result — content plus `ellipsis` — is exactly `max` characters (or just the
|
|
67
|
+
* ellipsis, when `max` is smaller than the ellipsis itself). Replacement for
|
|
68
|
+
* `str.slice(0, n) + '…'`, which can cut an emoji in half.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```
|
|
72
|
+
* truncateCharacters('abcdefghij', 5) // 'abcd…'
|
|
73
|
+
* truncateCharacters('abcdefghij', 8, '...') // 'abcde...'
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function truncateCharacters(str, max, ellipsis = '…') {
|
|
77
|
+
const characters = splitCharacters(str);
|
|
78
|
+
if (characters.length <= max) {
|
|
79
|
+
return str;
|
|
80
|
+
}
|
|
81
|
+
const keep = Math.max(max - characterCount(ellipsis), 0);
|
|
82
|
+
return characters.slice(0, keep).join('') + ellipsis;
|
|
83
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { parseISOTime, formatISOTime, formatDisplayTime12h, formatDisplayTime24h
|
|
|
13
13
|
export type { ISOTimeString, ParsedTime } from './timeParser';
|
|
14
14
|
export { parseStyleKey } from './parseStyleKey';
|
|
15
15
|
export { getKey, type Key, type KeyFallback } from './getKey';
|
|
16
|
+
export { characterCount, firstCharacter, truncateCharacters } from './characters';
|
|
16
17
|
export { mergeProps } from './mergeProps';
|
|
17
18
|
export { mergeRefs } from './mergeRefs';
|
|
18
19
|
export { isFocusDetached } from './focusReturn';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,YAAY,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,YAAY,EAAC,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAC,MAAM,aAAa,CAAC;AAEhF,OAAO,EACL,cAAc,EACd,SAAS,EACT,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,wBAAwB,EACxB,uBAAuB,EACvB,8BAA8B,EAC9B,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,8BAA8B,EAC9B,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAC,gBAAgB,EAAC,MAAM,aAAa,CAAC;AAElD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,aAAa,EACb,SAAS,EACT,UAAU,EACV,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAC,MAAM,EAAE,KAAK,GAAG,EAAE,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAE5D,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAC;AACxC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAC,UAAU,EAAE,mBAAmB,EAAC,MAAM,cAAc,CAAC;AAC7D,YAAY,EACV,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,cAAc,CAAC;AACtD,YAAY,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAC,aAAa,EAAE,eAAe,EAAC,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AACzC,YAAY,EAAC,SAAS,EAAE,mBAAmB,EAAC,MAAM,aAAa,CAAC;AAEhE,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,YAAY,EAAC,IAAI,EAAC,MAAM,SAAS,CAAC;AAElC,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAE3E,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAItC,OAAO,EAAC,kBAAkB,EAAE,iBAAiB,EAAC,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,YAAY,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,YAAY,EAAC,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAC,MAAM,aAAa,CAAC;AAEhF,OAAO,EACL,cAAc,EACd,SAAS,EACT,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,wBAAwB,EACxB,uBAAuB,EACvB,8BAA8B,EAC9B,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,8BAA8B,EAC9B,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAC,gBAAgB,EAAC,MAAM,aAAa,CAAC;AAElD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,aAAa,EACb,SAAS,EACT,UAAU,EACV,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAC,MAAM,EAAE,KAAK,GAAG,EAAE,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAE5D,OAAO,EAAC,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhF,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAC;AACxC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAC,UAAU,EAAE,mBAAmB,EAAC,MAAM,cAAc,CAAC;AAC7D,YAAY,EACV,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,cAAc,CAAC;AACtD,YAAY,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAC,aAAa,EAAE,eAAe,EAAC,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AACzC,YAAY,EAAC,SAAS,EAAE,mBAAmB,EAAC,MAAM,aAAa,CAAC;AAEhE,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,YAAY,EAAC,IAAI,EAAC,MAAM,SAAS,CAAC;AAElC,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAE3E,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAItC,OAAO,EAAC,kBAAkB,EAAE,iBAAiB,EAAC,MAAM,uBAAuB,CAAC"}
|
package/dist/utils/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { plainDateCreate, plainDateFromISO, plainDateToISO, plainDateToDate, pla
|
|
|
12
12
|
export { parseISOTime, formatISOTime, formatDisplayTime12h, formatDisplayTime24h, parseTimeInput, compareTime, isTimeInRange, clampTime, adjustTime, createISOTimeString } from "./timeParser.js";
|
|
13
13
|
export { parseStyleKey } from "./parseStyleKey.js";
|
|
14
14
|
export { getKey } from "./getKey.js";
|
|
15
|
+
export { characterCount, firstCharacter, truncateCharacters } from "./characters.js";
|
|
15
16
|
export { mergeProps } from "./mergeProps.js";
|
|
16
17
|
export { mergeRefs } from "./mergeRefs.js";
|
|
17
18
|
export { isFocusDetached } from "./focusReturn.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3-canary.5939961",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -654,6 +654,7 @@
|
|
|
654
654
|
"react-dom": ">=19.0.0"
|
|
655
655
|
},
|
|
656
656
|
"devDependencies": {
|
|
657
|
+
"@astryxdesign/cli": "0.4.3-canary.5939961",
|
|
657
658
|
"@babel/cli": "^8.0.4",
|
|
658
659
|
"@babel/core": "^7.29.7",
|
|
659
660
|
"@babel/preset-react": "^8.0.1",
|
|
@@ -662,8 +663,7 @@
|
|
|
662
663
|
"@testing-library/dom": "^10.0.0",
|
|
663
664
|
"@testing-library/jest-dom": "^6.6.0",
|
|
664
665
|
"@testing-library/react": "^16.3.2",
|
|
665
|
-
"rimraf": "^6.0.1"
|
|
666
|
-
"@astryxdesign/cli": "0.4.2"
|
|
666
|
+
"rimraf": "^6.0.1"
|
|
667
667
|
},
|
|
668
668
|
"dependencies": {
|
|
669
669
|
"intl-messageformat": "^11.2.9"
|
|
@@ -91,11 +91,16 @@ describe('Avatar', () => {
|
|
|
91
91
|
expect(screen.getByTestId('avatar')).toHaveTextContent('😀A');
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
it('preserves a complete
|
|
94
|
+
it('preserves a complete character when generating initials', () => {
|
|
95
95
|
render(<Avatar name="🇬🇧 Ada" data-testid="avatar" />);
|
|
96
96
|
expect(screen.getByTestId('avatar')).toHaveTextContent('🇬🇧A');
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
it('keeps a ZWJ family emoji intact when generating initials', () => {
|
|
100
|
+
render(<Avatar name="👨👩👧👦 Ada" data-testid="avatar" />);
|
|
101
|
+
expect(screen.getByTestId('avatar')).toHaveTextContent('👨👩👧👦A');
|
|
102
|
+
});
|
|
103
|
+
|
|
99
104
|
it('retries a new src after a previous src failed to load', () => {
|
|
100
105
|
const {rerender} = render(
|
|
101
106
|
<Avatar name="Ada" src="https://example.com/broken.jpg" />,
|
package/src/Avatar/Avatar.tsx
CHANGED
|
@@ -31,6 +31,7 @@ import {AvatarSizeContext} from './AvatarSizeContext';
|
|
|
31
31
|
import {useAvatarGroup} from '../AvatarGroup/AvatarGroupContext';
|
|
32
32
|
import {mergeProps, mergeRefs} from '../utils';
|
|
33
33
|
import {themeProps} from '../utils/themeProps';
|
|
34
|
+
import {firstCharacter} from '../utils/characters';
|
|
34
35
|
import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
35
36
|
import {useTooltip} from '../Tooltip/useTooltip';
|
|
36
37
|
import {useDevWarning} from '../hooks/useDevWarning';
|
|
@@ -317,25 +318,6 @@ export interface AvatarProps extends BaseProps<HTMLDivElement> {
|
|
|
317
318
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
318
319
|
}
|
|
319
320
|
|
|
320
|
-
/**
|
|
321
|
-
* Reuse a single segmenter when the runtime supports Intl.Segmenter.
|
|
322
|
-
*/
|
|
323
|
-
const graphemeSegmenter =
|
|
324
|
-
typeof Intl.Segmenter === 'function'
|
|
325
|
-
? new Intl.Segmenter(undefined, {granularity: 'grapheme'})
|
|
326
|
-
: null;
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Return the first user-perceived character, with a code-point fallback.
|
|
330
|
-
*/
|
|
331
|
-
function firstGrapheme(word: string): string {
|
|
332
|
-
if (graphemeSegmenter) {
|
|
333
|
-
return [...graphemeSegmenter.segment(word)][0]?.segment ?? '';
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return [...word][0] ?? '';
|
|
337
|
-
}
|
|
338
|
-
|
|
339
321
|
/**
|
|
340
322
|
* Generates initials from a name string.
|
|
341
323
|
* Takes the first letter of the first two words.
|
|
@@ -351,10 +333,10 @@ function getInitials(name: string): string {
|
|
|
351
333
|
return '';
|
|
352
334
|
}
|
|
353
335
|
if (words.length === 1) {
|
|
354
|
-
return
|
|
336
|
+
return firstCharacter(words[0]).toUpperCase();
|
|
355
337
|
}
|
|
356
338
|
return (
|
|
357
|
-
|
|
339
|
+
firstCharacter(words[0]) + firstCharacter(words[words.length - 1])
|
|
358
340
|
).toUpperCase();
|
|
359
341
|
}
|
|
360
342
|
|
|
@@ -17,11 +17,13 @@ export const docs = {
|
|
|
17
17
|
{guidance: true, description: 'Keep titles short and scannable: "Payment failed" not "There was a problem processing your most recent payment."'},
|
|
18
18
|
{guidance: false, description: 'Use Banner for short-lived messages that disappear on their own; use Toast instead.'},
|
|
19
19
|
{guidance: false, description: 'Stack multiple banners with the same status; combine related messages into one banner.'},
|
|
20
|
+
{guidance: true, description: 'Error and warning banners render as role="alert"; info and success render as role="status". Mount an alert banner in response to an event rather than on first paint, so assistive tech has a change to report.'},
|
|
21
|
+
{guidance: false, description: 'Rely on the status color or icon alone to carry meaning; say which status it is in the title text, because the icon is decorative to a screen reader.'},
|
|
20
22
|
],
|
|
21
23
|
anatomy: [
|
|
22
24
|
{name: 'Icon', required: true, description: 'Automatically set based on the status (info, warning, error, success).'},
|
|
23
|
-
{name: 'Title', required:
|
|
24
|
-
{name: 'Description', required: false, description: 'Additional detail below the title.
|
|
25
|
+
{name: 'Title', required: true, description: 'The main message. Always required.'},
|
|
26
|
+
{name: 'Description', required: false, description: 'Additional detail below the title.'},
|
|
25
27
|
{name: 'Action button', required: false, description: 'A button for the user to act on the message, like "Review" or "Retry".'},
|
|
26
28
|
{name: 'Dismiss button', required: false, description: 'Lets the user close the banner. Enabled by setting isDismissable.'},
|
|
27
29
|
{name: 'Collapsible content', required: false, description: 'Extra detail that expands below the banner header, like a list of errors.'},
|
|
@@ -123,7 +125,7 @@ export const docs = {
|
|
|
123
125
|
{className: 'astryx-banner-content', visualProps: ['container', 'status']},
|
|
124
126
|
],
|
|
125
127
|
vars: [
|
|
126
|
-
{name: '--_banner-radius', description: 'Border radius
|
|
128
|
+
{name: '--_banner-radius', description: 'Border radius of the card container (header, content area and the elevated root silhouette)', default: 'var(--radius-container)', private: true},
|
|
127
129
|
],
|
|
128
130
|
derived: [
|
|
129
131
|
{property: 'borderRadius', vars: ['--_banner-radius']},
|
|
@@ -148,8 +150,8 @@ export const docsZh = {
|
|
|
148
150
|
],
|
|
149
151
|
anatomy: [
|
|
150
152
|
{name: 'Icon', required: true, description: 'Automatically set based on the status (info, warning, error, success).'},
|
|
151
|
-
{name: 'Title', required:
|
|
152
|
-
{name: 'Description', required: false, description: 'Additional detail below the title.
|
|
153
|
+
{name: 'Title', required: true, description: 'The main message. Always required.'},
|
|
154
|
+
{name: 'Description', required: false, description: 'Additional detail below the title.'},
|
|
153
155
|
{name: 'Action button', required: false, description: 'A button for the user to act on the message, like "Review" or "Retry".'},
|
|
154
156
|
{name: 'Dismiss button', required: false, description: 'Lets the user close the banner. Enabled by setting isDismissable.'},
|
|
155
157
|
{name: 'Collapsible content', required: false, description: 'Extra detail that expands below the banner header, like a list of errors.'},
|
|
@@ -198,7 +200,7 @@ export const docsZh = {
|
|
|
198
200
|
},
|
|
199
201
|
],
|
|
200
202
|
vars: [
|
|
201
|
-
{name: '--_banner-radius', description: 'Border radius
|
|
203
|
+
{name: '--_banner-radius', description: 'Border radius of the card container (header, content area and the elevated root silhouette)', default: 'var(--radius-container)', private: true},
|
|
202
204
|
],
|
|
203
205
|
derived: [
|
|
204
206
|
{property: 'borderRadius', vars: ['--_banner-radius']},
|
|
@@ -222,7 +224,7 @@ export const docsDense = {
|
|
|
222
224
|
],
|
|
223
225
|
anatomy: [
|
|
224
226
|
{name: 'Icon', required: true, description: 'Set automatically from status.'},
|
|
225
|
-
{name: 'Title', required:
|
|
227
|
+
{name: 'Title', required: true, description: 'Main message text.'},
|
|
226
228
|
{name: 'Description', required: false, description: 'Detail below title.'},
|
|
227
229
|
{name: 'Action button', required: false, description: 'CTA like Review or Retry.'},
|
|
228
230
|
{name: 'Dismiss button', required: false, description: 'Close button via isDismissable.'},
|
|
@@ -455,4 +455,72 @@ describe('Banner', () => {
|
|
|
455
455
|
);
|
|
456
456
|
});
|
|
457
457
|
});
|
|
458
|
+
|
|
459
|
+
describe('dismiss focus handoff', () => {
|
|
460
|
+
it('returns focus to where it came from instead of dropping it to body', async () => {
|
|
461
|
+
const user = userEvent.setup();
|
|
462
|
+
render(
|
|
463
|
+
<>
|
|
464
|
+
<button type="button">Before</button>
|
|
465
|
+
<Banner status="info" title="Heads up" isDismissable />
|
|
466
|
+
</>,
|
|
467
|
+
);
|
|
468
|
+
const before = screen.getByRole('button', {name: 'Before'});
|
|
469
|
+
before.focus();
|
|
470
|
+
|
|
471
|
+
await user.tab();
|
|
472
|
+
expect(screen.getByRole('button', {name: 'Dismiss'})).toHaveFocus();
|
|
473
|
+
|
|
474
|
+
await user.keyboard('{Enter}');
|
|
475
|
+
|
|
476
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
477
|
+
expect(before).toHaveFocus();
|
|
478
|
+
expect(document.activeElement).not.toBe(document.body);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('leaves focus alone when it never entered the banner', async () => {
|
|
482
|
+
const user = userEvent.setup();
|
|
483
|
+
render(
|
|
484
|
+
<>
|
|
485
|
+
<button type="button">Elsewhere</button>
|
|
486
|
+
<Banner status="info" title="Heads up" isDismissable />
|
|
487
|
+
</>,
|
|
488
|
+
);
|
|
489
|
+
await user.click(screen.getByRole('button', {name: 'Dismiss'}));
|
|
490
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe('empty slots', () => {
|
|
495
|
+
it('does not show the expand affordance for children that render nothing', () => {
|
|
496
|
+
render(
|
|
497
|
+
<Banner status="info" title="Heads up">
|
|
498
|
+
{false}
|
|
499
|
+
</Banner>,
|
|
500
|
+
);
|
|
501
|
+
expect(
|
|
502
|
+
screen.queryByRole('button', {name: 'Expand'}),
|
|
503
|
+
).not.toBeInTheDocument();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('still shows the expand affordance for real children', () => {
|
|
507
|
+
render(
|
|
508
|
+
<Banner status="info" title="Heads up">
|
|
509
|
+
<p>Detail</p>
|
|
510
|
+
</Banner>,
|
|
511
|
+
);
|
|
512
|
+
expect(
|
|
513
|
+
screen.getByRole('button', {name: 'Expand'}),
|
|
514
|
+
).toBeInTheDocument();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('renders no description node for a description that renders nothing', () => {
|
|
518
|
+
const {container} = render(
|
|
519
|
+
<Banner status="info" title="Heads up" description="" />,
|
|
520
|
+
);
|
|
521
|
+
const header = container.firstElementChild!.firstElementChild!;
|
|
522
|
+
// icon wrapper + text column, and the text column holds the title alone
|
|
523
|
+
expect(header.children[1].children).toHaveLength(1);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
458
526
|
});
|
package/src/Banner/Banner.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Banner.tsx
|
|
7
|
-
* @input Uses React useState, Button, Icon (with registry string names), StyleX
|
|
7
|
+
* @input Uses React useState/useRef/useId, Button, Icon (with registry string names), StyleX
|
|
8
8
|
* @output Exports Banner component, BannerProps, BannerStatus, BannerContainer types
|
|
9
9
|
* @position Core implementation; consumed by index.ts, tested by Banner.test.tsx
|
|
10
10
|
*
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* - Each visual area owns its own border-radius (no overflow:clip on the container)
|
|
20
20
|
* - When children are provided, a collapse/expand toggle button appears in the end area
|
|
21
21
|
*
|
|
22
|
+
* A status added through `BannerStatusMap` augmentation has no entry in the
|
|
23
|
+
* status lookups, so it renders with no status fill, no default glyph and the
|
|
24
|
+
* polite `role="status"` rather than losing its ARIA role entirely.
|
|
25
|
+
*
|
|
22
26
|
* Title and description render as <div> (not <p>): they accept arbitrary
|
|
23
27
|
* ReactNode content, and <p> cannot legally contain block-level children
|
|
24
28
|
* (the HTML parser reparents them, desyncing SSR markup from the hydrated
|
|
@@ -34,12 +38,12 @@
|
|
|
34
38
|
* - /packages/cli/assets/templates/blocks/components/Banner/ (showcase blocks)
|
|
35
39
|
*/
|
|
36
40
|
|
|
37
|
-
import {useId, useState, type ReactNode} from 'react';
|
|
41
|
+
import {useId, useRef, useState, type ReactNode} from 'react';
|
|
38
42
|
import * as stylex from '@stylexjs/stylex';
|
|
39
43
|
import type {BaseProps} from '../BaseProps';
|
|
40
44
|
import {Button} from '../Button';
|
|
41
45
|
import {Icon} from '../Icon';
|
|
42
|
-
import type {IconName} from '../Icon';
|
|
46
|
+
import type {IconName, IconColor} from '../Icon';
|
|
43
47
|
import {
|
|
44
48
|
colorVars,
|
|
45
49
|
spacingVars,
|
|
@@ -51,7 +55,7 @@ import {
|
|
|
51
55
|
easeVars,
|
|
52
56
|
shadowVars,
|
|
53
57
|
} from '../theme/tokens.stylex';
|
|
54
|
-
import {mergeProps} from '../utils';
|
|
58
|
+
import {composeEventHandlers, isRenderable, mergeProps} from '../utils';
|
|
55
59
|
import type {Elevation} from '../utils/types';
|
|
56
60
|
import {edgeCompSlot} from '../Layout/edgeCompensation.stylex';
|
|
57
61
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -147,37 +151,38 @@ export interface BannerProps extends BaseProps<HTMLDivElement> {
|
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
// =============================================================================
|
|
150
|
-
// Status
|
|
154
|
+
// Status lookups
|
|
151
155
|
// =============================================================================
|
|
152
156
|
|
|
153
|
-
|
|
157
|
+
// `BannerStatus` is `keyof BannerStatusMap`, and index.ts documents augmenting
|
|
158
|
+
// that interface to add a status. Every lookup below is therefore partial: an
|
|
159
|
+
// augmented status the library has never heard of falls through to the base
|
|
160
|
+
// treatment (no status fill, no glyph, the polite role) instead of resolving to
|
|
161
|
+
// `undefined` and dropping the ARIA role along with it.
|
|
162
|
+
|
|
163
|
+
const defaultIconNames: Partial<Record<BannerStatus, IconName>> = {
|
|
154
164
|
info: 'info',
|
|
155
165
|
warning: 'warning',
|
|
156
166
|
error: 'error',
|
|
157
167
|
success: 'success',
|
|
158
168
|
};
|
|
159
169
|
|
|
160
|
-
|
|
161
|
-
// Status → ARIA role mapping
|
|
162
|
-
// =============================================================================
|
|
163
|
-
|
|
164
|
-
const statusRole: Record<BannerStatus, 'alert' | 'status'> = {
|
|
170
|
+
const statusRole: Partial<Record<BannerStatus, 'alert' | 'status'>> = {
|
|
165
171
|
info: 'status',
|
|
166
172
|
warning: 'alert',
|
|
167
173
|
error: 'alert',
|
|
168
174
|
success: 'status',
|
|
169
175
|
};
|
|
170
176
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// =============================================================================
|
|
177
|
+
/** An unknown status is not urgent by definition, so it announces politely. */
|
|
178
|
+
const FALLBACK_ROLE = 'status';
|
|
174
179
|
|
|
175
|
-
const statusIconColor = {
|
|
180
|
+
const statusIconColor: Partial<Record<BannerStatus, IconColor>> = {
|
|
176
181
|
info: 'accent',
|
|
177
182
|
warning: 'warning',
|
|
178
183
|
error: 'error',
|
|
179
184
|
success: 'success',
|
|
180
|
-
}
|
|
185
|
+
};
|
|
181
186
|
|
|
182
187
|
// =============================================================================
|
|
183
188
|
// Styles
|
|
@@ -193,7 +198,7 @@ const styles = stylex.create({
|
|
|
193
198
|
// When elevated, a card-container banner rounds its root so the shadow
|
|
194
199
|
// follows the same silhouette as the header/content border-radius.
|
|
195
200
|
rootElevatedCard: {
|
|
196
|
-
borderRadius: radiusVars['--radius-container']
|
|
201
|
+
borderRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
197
202
|
},
|
|
198
203
|
// Header area — colored status background with icon, title, description, actions
|
|
199
204
|
// This is the primary theme target ('banner')
|
|
@@ -206,11 +211,11 @@ const styles = stylex.create({
|
|
|
206
211
|
},
|
|
207
212
|
// Border-radius for card container: all corners when standalone, top-only when content is visible
|
|
208
213
|
headerCardStandalone: {
|
|
209
|
-
borderRadius: radiusVars['--radius-container']
|
|
214
|
+
borderRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
210
215
|
},
|
|
211
216
|
headerCardWithContent: {
|
|
212
|
-
borderStartStartRadius: radiusVars['--radius-container']
|
|
213
|
-
borderStartEndRadius: radiusVars['--radius-container']
|
|
217
|
+
borderStartStartRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
218
|
+
borderStartEndRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
214
219
|
borderEndStartRadius: 0,
|
|
215
220
|
borderEndEndRadius: 0,
|
|
216
221
|
},
|
|
@@ -233,6 +238,10 @@ const styles = stylex.create({
|
|
|
233
238
|
fontWeight: fontWeightVars['--font-weight-semibold'],
|
|
234
239
|
lineHeight: typeScaleVars['--text-label-leading'],
|
|
235
240
|
color: colorVars['--color-text-primary'],
|
|
241
|
+
// A single unbroken token (a URL, an ID, a German compound) otherwise sets
|
|
242
|
+
// the flex item's min-content width and pushes the page into horizontal
|
|
243
|
+
// scrolling at 320px, which is a WCAG 1.4.10 reflow failure.
|
|
244
|
+
overflowWrap: 'anywhere',
|
|
236
245
|
},
|
|
237
246
|
description: {
|
|
238
247
|
margin: 0,
|
|
@@ -241,6 +250,7 @@ const styles = stylex.create({
|
|
|
241
250
|
fontWeight: fontWeightVars['--font-weight-normal'],
|
|
242
251
|
lineHeight: typeScaleVars['--text-supporting-leading'],
|
|
243
252
|
color: colorVars['--color-text-secondary'],
|
|
253
|
+
overflowWrap: 'anywhere',
|
|
244
254
|
},
|
|
245
255
|
iconWrapper: {
|
|
246
256
|
display: 'flex',
|
|
@@ -262,17 +272,17 @@ const styles = stylex.create({
|
|
|
262
272
|
paddingInline: spacingVars['--spacing-4'],
|
|
263
273
|
borderInlineStartWidth: borderVars['--border-width'],
|
|
264
274
|
borderInlineEndWidth: borderVars['--border-width'],
|
|
265
|
-
|
|
275
|
+
borderBlockEndWidth: borderVars['--border-width'],
|
|
266
276
|
borderInlineStartStyle: 'solid',
|
|
267
277
|
borderInlineEndStyle: 'solid',
|
|
268
|
-
|
|
278
|
+
borderBlockEndStyle: 'solid',
|
|
269
279
|
borderInlineStartColor: colorVars['--color-border'],
|
|
270
280
|
borderInlineEndColor: colorVars['--color-border'],
|
|
271
|
-
|
|
281
|
+
borderBlockEndColor: colorVars['--color-border'],
|
|
272
282
|
},
|
|
273
283
|
contentAreaCard: {
|
|
274
|
-
borderEndStartRadius: radiusVars['--radius-container']
|
|
275
|
-
borderEndEndRadius: radiusVars['--radius-container']
|
|
284
|
+
borderEndStartRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
285
|
+
borderEndEndRadius: `var(--_banner-radius, ${radiusVars['--radius-container']})`,
|
|
276
286
|
},
|
|
277
287
|
// Applied to the chevron <Icon> itself (via `xstyle`) rather than a wrapper,
|
|
278
288
|
// so the element that rotates is the element a theme targets.
|
|
@@ -304,6 +314,17 @@ const statusStyles = stylex.create({
|
|
|
304
314
|
},
|
|
305
315
|
});
|
|
306
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Narrows an augmented `BannerStatus` to one the style map actually carries,
|
|
319
|
+
* so an unknown status renders with no status fill rather than crashing the
|
|
320
|
+
* lookup. StyleX style maps cannot be declared `Partial`, hence the guard.
|
|
321
|
+
*/
|
|
322
|
+
function hasStatusStyle(
|
|
323
|
+
status: BannerStatus,
|
|
324
|
+
): status is BannerStatus & keyof typeof statusStyles {
|
|
325
|
+
return Object.prototype.hasOwnProperty.call(statusStyles, status);
|
|
326
|
+
}
|
|
327
|
+
|
|
307
328
|
// Resting elevation for the banner. Applied to the root so the shadow wraps
|
|
308
329
|
// the whole banner (header + optional content). 'none' is the default and
|
|
309
330
|
// leaves the layout-only root untouched. For the `card` container the root is
|
|
@@ -381,28 +402,63 @@ export function Banner({
|
|
|
381
402
|
className,
|
|
382
403
|
style,
|
|
383
404
|
ref,
|
|
405
|
+
onFocusCapture,
|
|
406
|
+
onPointerDownCapture,
|
|
384
407
|
...rest
|
|
385
408
|
}: BannerProps) {
|
|
386
409
|
const t = useTranslator();
|
|
387
410
|
const [isDismissed, setIsDismissed] = useState(false);
|
|
388
411
|
const [isExpanded, setIsExpanded] = useState(defaultIsExpanded);
|
|
412
|
+
// The element focus came from before it entered the banner. Dismissing
|
|
413
|
+
// unmounts the whole banner, dismiss button included, so without a handoff
|
|
414
|
+
// the browser drops focus to <body> and a keyboard user loses their place.
|
|
415
|
+
// ToastViewport makes the same handoff when a focused toast is dismissed.
|
|
416
|
+
const focusOriginRef = useRef<HTMLElement | null>(null);
|
|
389
417
|
// Links the expand/collapse toggle to the content region it shows/hides so
|
|
390
418
|
// assistive tech can move from the button to its controlled content
|
|
391
419
|
// (disclosure pattern). The region is conditionally rendered, so aria-controls
|
|
392
420
|
// below is set only while it's mounted to avoid a dangling reference.
|
|
393
421
|
const contentId = useId();
|
|
394
422
|
const defaultIconName = defaultIconNames[status];
|
|
395
|
-
const role = statusRole[status];
|
|
423
|
+
const role = statusRole[status] ?? FALLBACK_ROLE;
|
|
396
424
|
const iconColor = statusIconColor[status];
|
|
397
|
-
const hasChildren = children
|
|
425
|
+
const hasChildren = isRenderable(children);
|
|
398
426
|
|
|
399
427
|
if (isDismissed) {
|
|
400
428
|
return null;
|
|
401
429
|
}
|
|
402
430
|
|
|
431
|
+
// `focusin` reports the element focus came *from* as relatedTarget; on
|
|
432
|
+
// pointerdown focus has not moved yet, so document.activeElement is it.
|
|
433
|
+
const rememberFocusOrigin = (candidate: EventTarget | null, root: Node) => {
|
|
434
|
+
if (
|
|
435
|
+
candidate instanceof HTMLElement &&
|
|
436
|
+
candidate !== document.body &&
|
|
437
|
+
!root.contains(candidate)
|
|
438
|
+
) {
|
|
439
|
+
focusOriginRef.current = candidate;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const handleFocusCapture = (event: React.FocusEvent<HTMLDivElement>) => {
|
|
444
|
+
rememberFocusOrigin(event.relatedTarget, event.currentTarget);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const handlePointerDownCapture = (
|
|
448
|
+
event: React.PointerEvent<HTMLDivElement>,
|
|
449
|
+
) => {
|
|
450
|
+
rememberFocusOrigin(document.activeElement, event.currentTarget);
|
|
451
|
+
};
|
|
452
|
+
|
|
403
453
|
const handleDismiss = () => {
|
|
454
|
+
const origin = focusOriginRef.current;
|
|
404
455
|
setIsDismissed(true);
|
|
405
456
|
onDismiss?.();
|
|
457
|
+
// Move focus before React removes the subtree, so the browser never has a
|
|
458
|
+
// frame where the focused node is gone.
|
|
459
|
+
if (origin?.isConnected) {
|
|
460
|
+
origin.focus();
|
|
461
|
+
}
|
|
406
462
|
};
|
|
407
463
|
|
|
408
464
|
const handleToggleExpand = () => {
|
|
@@ -410,11 +466,11 @@ export function Banner({
|
|
|
410
466
|
};
|
|
411
467
|
|
|
412
468
|
// Show the end area if there are actions, dismiss, or a collapsible toggle
|
|
413
|
-
const showEndArea = endContent
|
|
469
|
+
const showEndArea = isRenderable(endContent) || isDismissable || hasChildren;
|
|
414
470
|
// Center items vertically when there's only a title (no description)
|
|
415
471
|
// and the banner has action buttons
|
|
416
|
-
const hasActions = endContent
|
|
417
|
-
const isSingleLine = description
|
|
472
|
+
const hasActions = isRenderable(endContent) || isDismissable;
|
|
473
|
+
const isSingleLine = !isRenderable(description) && hasActions;
|
|
418
474
|
|
|
419
475
|
const showContent = hasChildren && isExpanded;
|
|
420
476
|
const isCard = container === 'card';
|
|
@@ -423,6 +479,11 @@ export function Banner({
|
|
|
423
479
|
<div
|
|
424
480
|
ref={ref}
|
|
425
481
|
role={role}
|
|
482
|
+
onFocusCapture={composeEventHandlers(onFocusCapture, handleFocusCapture)}
|
|
483
|
+
onPointerDownCapture={composeEventHandlers(
|
|
484
|
+
onPointerDownCapture,
|
|
485
|
+
handlePointerDownCapture,
|
|
486
|
+
)}
|
|
426
487
|
{...mergeProps(
|
|
427
488
|
stylex.props(
|
|
428
489
|
styles.root,
|
|
@@ -441,7 +502,7 @@ export function Banner({
|
|
|
441
502
|
stylex.props(
|
|
442
503
|
styles.header,
|
|
443
504
|
isSingleLine && styles.headerCentered,
|
|
444
|
-
statusStyles[status],
|
|
505
|
+
hasStatusStyle(status) && statusStyles[status],
|
|
445
506
|
isCard &&
|
|
446
507
|
(showContent
|
|
447
508
|
? styles.headerCardWithContent
|
|
@@ -454,16 +515,16 @@ export function Banner({
|
|
|
454
515
|
overrides reach it via inheritance). The wrapper itself stays
|
|
455
516
|
layout-only. */}
|
|
456
517
|
<div
|
|
457
|
-
{...(icon
|
|
518
|
+
{...(isRenderable(icon)
|
|
458
519
|
? mergeProps(
|
|
459
520
|
themeProps('banner-icon', {status}),
|
|
460
521
|
stylex.props(styles.iconWrapper),
|
|
461
522
|
)
|
|
462
523
|
: stylex.props(styles.iconWrapper))}
|
|
463
524
|
aria-hidden="true">
|
|
464
|
-
{icon
|
|
525
|
+
{isRenderable(icon) ? (
|
|
465
526
|
icon
|
|
466
|
-
) : (
|
|
527
|
+
) : defaultIconName != null ? (
|
|
467
528
|
// Applied to the status <Icon> itself rather than the wrapper, so
|
|
468
529
|
// the element that paints the glyph is the element a theme
|
|
469
530
|
// targets — a 'banner-icon' 'status:X' color override beats the
|
|
@@ -474,11 +535,11 @@ export function Banner({
|
|
|
474
535
|
color={iconColor}
|
|
475
536
|
{...themeProps('banner-icon', {status})}
|
|
476
537
|
/>
|
|
477
|
-
)}
|
|
538
|
+
) : null}
|
|
478
539
|
</div>
|
|
479
540
|
<div {...stylex.props(styles.headerContent)}>
|
|
480
541
|
<div {...stylex.props(styles.title)}>{title}</div>
|
|
481
|
-
{description
|
|
542
|
+
{isRenderable(description) && (
|
|
482
543
|
<div {...stylex.props(styles.description)}>{description}</div>
|
|
483
544
|
)}
|
|
484
545
|
</div>
|