@@ -9,153 +9,233 @@ import sx, {type SxProp} from '../../sx'
9
9
import type { ForwardRefComponent as PolymorphicForwardRefComponent } from '../../utils/polymorphic'
10
10
import { defaultSxProp } from '../../utils/defaultSxProp'
11
11
import { get } from '../../constants'
12
+ import { toggleStyledComponent } from '../utils/toggleStyledComponent'
13
+
14
+ import classes from './UnderlineTabbedInterface.module.css'
15
+ import { useFeatureFlag } from '../../FeatureFlags'
16
+ import { clsx } from 'clsx'
12
17
13
18
// The gap between the list items. It is a constant because the gap is used to calculate the possible number of items that can fit in the container.
14
19
export const GAP = 8
15
20
16
- export const StyledUnderlineWrapper = styled . div `
17
- display: flex;
18
- padding-inline: var(--stack-padding-normal, ${ get ( 'space.3' ) } );
19
- justify-content: flex-start;
20
- align-items: center;
21
- /* make space for the underline */
22
- min-height: var(--control-xlarge-size, 48px);
23
- /* using a box-shadow instead of a border to accomodate 'overflow-y: hidden' on UnderlinePanels */
24
- box-shadow: inset 0px -1px var(--borderColor-muted, ${ get ( 'colors.border.muted' ) } );
25
-
26
- ${ sx } ;
27
- `
21
+ const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team'
28
22
29
- export const StyledUnderlineItemList = styled . ul `
30
- display: flex;
31
- list-style: none;
32
- white-space: nowrap;
33
- padding: 0;
34
- margin: 0;
35
- align-items: center;
36
- gap: ${ GAP } px;
37
- position: relative;
38
- `
23
+ const StyledComponentUnderlineWrapper = toggleStyledComponent (
24
+ CSS_MODULES_FEATURE_FLAG ,
25
+ 'div' ,
26
+ styled . div `
27
+ display: flex;
28
+ padding-inline: var(--stack-padding-normal, ${ get ( 'space.3' ) } );
29
+ justify-content: flex-start;
30
+ align-items: center;
31
+ /* make space for the underline */
32
+ min-height: var(--control-xlarge-size, 48px);
33
+ /* using a box-shadow instead of a border to accomodate 'overflow-y: hidden' on UnderlinePanels */
34
+ box-shadow: inset 0px -1px var(--borderColor-muted, ${ get ( 'colors.border.muted' ) } );
39
35
40
- export const StyledUnderlineItem = styled . div `
41
- /* button resets */
42
- appearance: none;
43
- background-color: transparent;
44
- border: 0;
45
- cursor: pointer;
46
- font: inherit;
47
-
48
- /* underline tab specific styles */
49
- position: relative;
50
- display: inline-flex;
51
- color: ${ get ( 'colors.fg.default' ) } ;
52
- text-align: center;
53
- text-decoration: none;
54
- line-height: var(--text-body-lineHeight-medium, 1.4285);
55
- border-radius: var(--borderRadius-medium, ${ get ( 'radii.2' ) } );
56
- font-size: var(--text-body-size-medium, ${ get ( 'fontSizes.1' ) } );
57
- padding-inline: var(--control-medium-paddingInline-condensed, ${ get ( 'space.2' ) } );
58
- padding-block: var(--control-medium-paddingBlock, 6px);
59
- align-items: center;
60
-
61
- @media (hover: hover) {
62
- &:hover {
63
- background-color: var(--bgColor-neutral-muted, ${ get ( 'colors.neutral.subtle' ) } );
64
- transition: background 0.12s ease-out;
65
- text-decoration: none;
66
- }
67
- }
36
+ ${ sx } ;
37
+ ` ,
38
+ )
68
39
69
- &:focus: {
70
- outline: 2px solid transparent;
71
- box-shadow: inset 0 0 0 2px var(--fgColor-accent, ${ get ( 'colors.accent.fg' ) } );
40
+ type StyledUnderlineWrapperProps = {
41
+ slot ?: string
42
+ as ?: React . ElementType
43
+ className ?: string
44
+ } & SxProp
72
45
73
- /* where focus-visible is supported, remove the focus box-shadow */
74
- &:not(:focus-visible) {
75
- box-shadow: none;
76
- }
77
- }
46
+ export const StyledUnderlineWrapper = forwardRef (
47
+ ( { children, className, ...rest } : PropsWithChildren < StyledUnderlineWrapperProps > , forwardedRef ) => {
48
+ const enabled = useFeatureFlag ( CSS_MODULES_FEATURE_FLAG )
78
49
79
- &:focus-visible {
80
- outline: 2px solid transparent;
81
- box-shadow: inset 0 0 0 2px var(--fgColor-accent, ${ get ( 'colors.accent.fg' ) } );
82
- }
50
+ if ( enabled ) {
51
+ return (
52
+ < StyledComponentUnderlineWrapper
53
+ className = { clsx ( classes . UnderlineWrapper , className ) }
54
+ ref = { forwardedRef }
55
+ { ...rest }
56
+ >
57
+ { children }
58
+ </ StyledComponentUnderlineWrapper >
59
+ )
60
+ }
61
+ return (
62
+ < StyledComponentUnderlineWrapper className = { className } ref = { forwardedRef } { ...rest } >
63
+ { children }
64
+ </ StyledComponentUnderlineWrapper >
65
+ )
66
+ } ,
67
+ )
83
68
84
- /* renders a visibly hidden "copy" of the label in bold, reserving box space for when label becomes bold on selected */
85
- [data-content]::before {
86
- content: attr(data-content);
87
- display: block;
88
- height: 0;
89
- font-weight: var(--base-text-weight-semibold, ${ get ( 'fontWeights.semibold' ) } );
90
- visibility: hidden;
69
+ const StyledComponentUnderlineItemList = toggleStyledComponent (
70
+ CSS_MODULES_FEATURE_FLAG ,
71
+ 'ul' ,
72
+ styled . ul `
73
+ display: flex;
74
+ list-style: none;
91
75
white-space: nowrap;
92
- }
93
-
94
- [data-component='icon'] {
95
- color: var(--fgColor-muted, ${ get ( 'colors.fg.muted' ) } );
76
+ padding: 0;
77
+ margin: 0;
96
78
align-items: center;
97
- display: inline-flex;
98
- margin-inline-end: var(--control-medium-gap, ${ get ( 'space.2' ) } );
99
- }
79
+ gap: ${ GAP } px;
80
+ position: relative;
81
+ ` ,
82
+ )
100
83
101
- [data-component='counter'] {
102
- margin-inline-start: var(--control-medium-gap, ${ get ( 'space.2' ) } );
103
- display: flex;
104
- align-items: center;
84
+ export const StyledUnderlineItemList = forwardRef ( ( { children, ...rest } : PropsWithChildren , forwardedRef ) => {
85
+ const enabled = useFeatureFlag ( CSS_MODULES_FEATURE_FLAG )
86
+
87
+ if ( enabled ) {
88
+ return (
89
+ < StyledComponentUnderlineItemList className = { classes . UnderlineItemList } ref = { forwardedRef } { ...rest } >
90
+ { children }
91
+ </ StyledComponentUnderlineItemList >
92
+ )
105
93
}
106
94
107
- /* selected state styles */
108
- &::after {
109
- position: absolute;
110
- right: 50%;
111
- /* TODO: see if we can simplify this positioning */
112
- /* 48px total height / 2 (24px) + 1px */
113
- bottom: calc(50% - calc(var(--control-xlarge-size, 48px) / 2 + 1px));
114
- width: 100%;
115
- height: 2px;
116
- content: '';
95
+ return (
96
+ < StyledComponentUnderlineItemList ref = { forwardedRef } { ...rest } >
97
+ { children }
98
+ </ StyledComponentUnderlineItemList >
99
+ )
100
+ } ) as PolymorphicForwardRefComponent < 'ul' >
101
+
102
+ export const StyledUnderlineItem = toggleStyledComponent (
103
+ CSS_MODULES_FEATURE_FLAG ,
104
+ 'div' ,
105
+ styled . div `
106
+ /* button resets */
107
+ appearance: none;
117
108
background-color: transparent;
118
- border-radius: 0;
119
- transform: translate(50%, -50%);
120
- }
109
+ border: 0;
110
+ cursor: pointer;
111
+ font: inherit;
112
+
113
+ /* underline tab specific styles */
114
+ position: relative;
115
+ display: inline-flex;
116
+ color: ${ get ( 'colors.fg.default' ) } ;
117
+ text-align: center;
118
+ text-decoration: none;
119
+ line-height: var(--text-body-lineHeight-medium, 1.4285);
120
+ border-radius: var(--borderRadius-medium, ${ get ( 'radii.2' ) } );
121
+ font-size: var(--text-body-size-medium, ${ get ( 'fontSizes.1' ) } );
122
+ padding-inline: var(--control-medium-paddingInline-condensed, ${ get ( 'space.2' ) } );
123
+ padding-block: var(--control-medium-paddingBlock, 6px);
124
+ align-items: center;
121
125
122
- &[aria-current]:not([aria-current='false']),
123
- &[aria-selected='true'] {
124
- [data-component='text'] {
126
+ @media (hover: hover) {
127
+ &:hover {
128
+ background-color: var(--bgColor-neutral-muted, ${ get ( 'colors.neutral.subtle' ) } );
129
+ transition: background 0.12s ease-out;
130
+ text-decoration: none;
131
+ }
132
+ }
133
+
134
+ &:focus: {
135
+ outline: 2px solid transparent;
136
+ box-shadow: inset 0 0 0 2px var(--fgColor-accent, ${ get ( 'colors.accent.fg' ) } );
137
+
138
+ /* where focus-visible is supported, remove the focus box-shadow */
139
+ &:not(:focus-visible) {
140
+ box-shadow: none;
141
+ }
142
+ }
143
+
144
+ &:focus-visible {
145
+ outline: 2px solid transparent;
146
+ box-shadow: inset 0 0 0 2px var(--fgColor-accent, ${ get ( 'colors.accent.fg' ) } );
147
+ }
148
+
149
+ /* renders a visibly hidden "copy" of the label in bold, reserving box space for when label becomes bold on selected */
150
+ [data-content]::before {
151
+ content: attr(data-content);
152
+ display: block;
153
+ height: 0;
125
154
font-weight: var(--base-text-weight-semibold, ${ get ( 'fontWeights.semibold' ) } );
155
+ visibility: hidden;
156
+ white-space: nowrap;
157
+ }
158
+
159
+ [data-component='icon'] {
160
+ color: var(--fgColor-muted, ${ get ( 'colors.fg.muted' ) } );
161
+ align-items: center;
162
+ display: inline-flex;
163
+ margin-inline-end: var(--control-medium-gap, ${ get ( 'space.2' ) } );
126
164
}
127
165
166
+ [data-component='counter'] {
167
+ margin-inline-start: var(--control-medium-gap, ${ get ( 'space.2' ) } );
168
+ display: flex;
169
+ align-items: center;
170
+ }
171
+
172
+ /* selected state styles */
128
173
&::after {
129
- background-color: var(--underlineNav-borderColor-active, var(--color-primer-border-active, #fd8c73));
174
+ position: absolute;
175
+ right: 50%;
176
+ /* TODO: see if we can simplify this positioning */
177
+ /* 48px total height / 2 (24px) + 1px */
178
+ bottom: calc(50% - calc(var(--control-xlarge-size, 48px) / 2 + 1px));
179
+ width: 100%;
180
+ height: 2px;
181
+ content: '';
182
+ background-color: transparent;
183
+ border-radius: 0;
184
+ transform: translate(50%, -50%);
130
185
}
131
- }
132
186
133
- @media (forced-colors: active) {
134
187
&[aria-current]:not([aria-current='false']),
135
188
&[aria-selected='true'] {
136
- ::after {
137
- // Support for Window Force Color Mode https://learn.microsoft.com/en-us/fluent-ui/web-components/design-system/high-contrast
138
- background-color: LinkText;
189
+ [data-component='text'] {
190
+ font-weight: var(--base-text-weight-semibold, ${ get ( 'fontWeights.semibold' ) } );
191
+ }
192
+
193
+ &::after {
194
+ background-color: var(--underlineNav-borderColor-active, var(--color-primer-border-active, #fd8c73));
139
195
}
140
196
}
141
- }
142
- ${ sx } ;
143
- `
197
+
198
+ @media (forced-colors: active) {
199
+ &[aria-current]:not([aria-current='false']),
200
+ &[aria-selected='true'] {
201
+ ::after {
202
+ // Support for Window Force Color Mode https://learn.microsoft.com/en-us/fluent-ui/web-components/design-system/high-contrast
203
+ background-color: LinkText;
204
+ }
205
+ }
206
+ }
207
+ ${ sx } ;
208
+ ` ,
209
+ )
144
210
145
211
const loadingKeyframes = keyframes `
146
212
from { opacity: 1; }
147
213
to { opacity: 0.2; }
148
214
`
149
215
150
- export const LoadingCounter = styled . span `
151
- animation: ${ loadingKeyframes } 1.2s ease-in-out infinite alternate;
152
- background-color: var(--bgColor-neutral-muted, ${ get ( 'colors.neutral.subtle' ) } );
153
- border-color: var(--borderColor-default, ${ get ( 'colors.border.default' ) } );
154
- width: 1.5rem;
155
- height: 1rem; /*16px*/
156
- display: inline-block;
157
- border-radius: 20px;
158
- `
216
+ export const StyledComponentLoadingCounter = toggleStyledComponent (
217
+ CSS_MODULES_FEATURE_FLAG ,
218
+ 'span' ,
219
+ styled . span `
220
+ animation: ${ loadingKeyframes } 1.2s ease-in-out infinite alternate;
221
+ background-color: var(--bgColor-neutral-muted, ${ get ( 'colors.neutral.subtle' ) } );
222
+ border-color: var(--borderColor-default, ${ get ( 'colors.border.default' ) } );
223
+ width: 1.5rem;
224
+ height: 1rem; /*16px*/
225
+ display: inline-block;
226
+ border-radius: 20px;
227
+ ` ,
228
+ )
229
+
230
+ export const LoadingCounter = ( ) => {
231
+ const enabled = useFeatureFlag ( CSS_MODULES_FEATURE_FLAG )
232
+
233
+ if ( enabled ) {
234
+ return < StyledComponentLoadingCounter className = { classes . LoadingCounter } />
235
+ }
236
+
237
+ return < StyledComponentLoadingCounter />
238
+ }
159
239
160
240
// We can uncomment these when/if we add overflow behavior
161
241
// to the UnderlinePanels component
@@ -212,6 +292,32 @@ export const UnderlineItem = forwardRef(
212
292
} : PropsWithChildren < UnderlineItemProps > ,
213
293
forwardedRef ,
214
294
) => {
295
+ const enabled = useFeatureFlag ( CSS_MODULES_FEATURE_FLAG )
296
+
297
+ if ( enabled ) {
298
+ return (
299
+ < StyledUnderlineItem ref = { forwardedRef } as = { as } sx = { sxProp } className = { classes . UnderlineItem } { ...rest } >
300
+ { iconsVisible && Icon && < span data-component = "icon" > { isElement ( Icon ) ? Icon : < Icon /> } </ span > }
301
+ { children && (
302
+ < span data-component = "text" data-content = { children } >
303
+ { children }
304
+ </ span >
305
+ ) }
306
+ { counter !== undefined ? (
307
+ loadingCounters ? (
308
+ < span data-component = "counter" >
309
+ < LoadingCounter />
310
+ </ span >
311
+ ) : (
312
+ < span data-component = "counter" >
313
+ < CounterLabel > { counter } </ CounterLabel >
314
+ </ span >
315
+ )
316
+ ) : null }
317
+ </ StyledUnderlineItem >
318
+ )
319
+ }
320
+
215
321
return (
216
322
< StyledUnderlineItem ref = { forwardedRef } as = { as } sx = { sxProp } { ...rest } >
217
323
{ iconsVisible && Icon && < span data-component = "icon" > { isElement ( Icon ) ? Icon : < Icon /> } </ span > }
0 commit comments