@gitlab/ui 32.28.0 → 32.30.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 +28 -0
- package/dist/components/base/avatar/avatar.js +2 -1
- package/dist/components/base/toggle/toggle.js +14 -10
- package/dist/utility_classes.css +1 -1
- package/dist/utility_classes.css.map +1 -1
- package/dist/utils/string_utils.js +16 -1
- package/package.json +1 -1
- package/src/components/base/avatar/avatar.stories.js +30 -0
- package/src/components/base/avatar/avatar.vue +2 -1
- package/src/components/base/toggle/toggle.spec.js +14 -9
- package/src/components/base/toggle/toggle.stories.js +3 -0
- package/src/components/base/toggle/toggle.vue +38 -31
- package/src/scss/mixins.scss +2 -2
- package/src/scss/utilities.scss +32 -0
- package/src/scss/utility-mixins/border.scss +8 -0
- package/src/utils/string_utils.js +14 -0
- package/src/utils/string_utils.spec.js +19 -1
|
@@ -60,5 +60,20 @@ const splitAfterSymbols = (symbols, string) => {
|
|
|
60
60
|
|
|
61
61
|
return textParts;
|
|
62
62
|
};
|
|
63
|
+
const getAvatarChar = name => {
|
|
64
|
+
if (name) {
|
|
65
|
+
// Check if first character is an emjoi
|
|
66
|
+
const match = name.match(/^\p{Emoji}/u);
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
if (match) {
|
|
69
|
+
// Return the first match
|
|
70
|
+
return match[0];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return name.charAt(0).toUpperCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return '';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export { getAvatarChar, splitAfterSymbols };
|
package/package.json
CHANGED
|
@@ -43,6 +43,26 @@ function generateProjectFallbackProps() {
|
|
|
43
43
|
return props;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function generateEmojiProjectProps() {
|
|
47
|
+
const defaultSize = avatarSizeOptions[1];
|
|
48
|
+
const props = {
|
|
49
|
+
entityId: {
|
|
50
|
+
type: Number,
|
|
51
|
+
default: number('entityId', 123),
|
|
52
|
+
},
|
|
53
|
+
entityName: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: text('entityName', '🦊Tanuki'),
|
|
56
|
+
},
|
|
57
|
+
size: {
|
|
58
|
+
type: Number,
|
|
59
|
+
default: select('size', avatarSizeOptions, defaultSize),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return props;
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
function generateTooltipProps() {
|
|
47
67
|
const props = {
|
|
48
68
|
tooltipText: {
|
|
@@ -80,6 +100,16 @@ documentedStoriesOf('base/avatar', readme)
|
|
|
80
100
|
shape="rect" />
|
|
81
101
|
`,
|
|
82
102
|
}))
|
|
103
|
+
.add('emoji-project-name', () => ({
|
|
104
|
+
props: generateEmojiProjectProps(),
|
|
105
|
+
template: `
|
|
106
|
+
<gl-avatar
|
|
107
|
+
:entity-name="entityName"
|
|
108
|
+
:entity-id="entityId"
|
|
109
|
+
:size="size"
|
|
110
|
+
shape="rect" />
|
|
111
|
+
`,
|
|
112
|
+
}))
|
|
83
113
|
.add('with-tooltip', () => ({
|
|
84
114
|
props: {
|
|
85
115
|
...generateImageProps(),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { avatarShapeOptions, avatarSizeOptions } from '../../../utils/constants';
|
|
3
|
+
import { getAvatarChar } from '../../../utils/string_utils';
|
|
3
4
|
|
|
4
5
|
const IDENTICON_BG_COUNT = 7;
|
|
5
6
|
|
|
@@ -62,7 +63,7 @@ export default {
|
|
|
62
63
|
return `gl-avatar-identicon-bg${type}`;
|
|
63
64
|
},
|
|
64
65
|
identiconText() {
|
|
65
|
-
return this.entityName
|
|
66
|
+
return getAvatarChar(this.entityName);
|
|
66
67
|
},
|
|
67
68
|
},
|
|
68
69
|
};
|
|
@@ -8,12 +8,14 @@ describe('toggle', () => {
|
|
|
8
8
|
let wrapper;
|
|
9
9
|
|
|
10
10
|
const label = 'toggle label';
|
|
11
|
+
const labelId = 'toggle-label-id';
|
|
11
12
|
const helpText = 'help text';
|
|
12
13
|
|
|
13
14
|
const createWrapper = (props = {}) => {
|
|
14
15
|
wrapper = shallowMount(Toggle, {
|
|
15
16
|
propsData: {
|
|
16
17
|
label,
|
|
18
|
+
labelId,
|
|
17
19
|
...props,
|
|
18
20
|
},
|
|
19
21
|
});
|
|
@@ -24,7 +26,6 @@ describe('toggle', () => {
|
|
|
24
26
|
|
|
25
27
|
afterEach(() => {
|
|
26
28
|
wrapper.destroy();
|
|
27
|
-
wrapper = null;
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
it('has role=switch', () => {
|
|
@@ -112,21 +113,25 @@ describe('toggle', () => {
|
|
|
112
113
|
|
|
113
114
|
describe('label position', () => {
|
|
114
115
|
describe.each`
|
|
115
|
-
state | labelPosition |
|
|
116
|
-
${'top'} | ${toggleLabelPosition.top} | ${
|
|
117
|
-
${'left'} | ${toggleLabelPosition.left} | ${
|
|
118
|
-
${'hidden'} | ${toggleLabelPosition.hidden} | ${
|
|
119
|
-
`('when $state', ({ labelPosition,
|
|
116
|
+
state | labelPosition | hasGlSrOnlyClass
|
|
117
|
+
${'top'} | ${toggleLabelPosition.top} | ${false}
|
|
118
|
+
${'left'} | ${toggleLabelPosition.left} | ${false}
|
|
119
|
+
${'hidden'} | ${toggleLabelPosition.hidden} | ${true}
|
|
120
|
+
`('when $state', ({ labelPosition, hasGlSrOnlyClass }) => {
|
|
120
121
|
beforeEach(() => {
|
|
121
122
|
createWrapper({ labelPosition });
|
|
122
123
|
});
|
|
123
124
|
|
|
124
|
-
it(`${
|
|
125
|
-
|
|
125
|
+
it(`${hasGlSrOnlyClass ? 'adds' : 'does not add'} 'gl-sr-only' class to the label`, () => {
|
|
126
|
+
const cssClasses = wrapper.find('[data-testid="toggle-label"]').classes();
|
|
127
|
+
|
|
128
|
+
return hasGlSrOnlyClass
|
|
129
|
+
? expect(cssClasses).toContain('gl-sr-only')
|
|
130
|
+
: expect(cssClasses).not.toContain('gl-sr-only');
|
|
126
131
|
});
|
|
127
132
|
|
|
128
133
|
it('has accessible name for the button', () => {
|
|
129
|
-
expect(findButton().attributes('aria-
|
|
134
|
+
expect(findButton().attributes('aria-labelledby')).toBe(labelId);
|
|
130
135
|
});
|
|
131
136
|
});
|
|
132
137
|
});
|
|
@@ -9,6 +9,7 @@ const generateProps = ({
|
|
|
9
9
|
disabled = defaultValue('disabled'),
|
|
10
10
|
isLoading = defaultValue('isLoading'),
|
|
11
11
|
label = 'Dark mode',
|
|
12
|
+
labelId = 'dark-mode-toggle',
|
|
12
13
|
help = 'Toggle dark mode for the website',
|
|
13
14
|
labelPosition = defaultValue('labelPosition'),
|
|
14
15
|
} = {}) => ({
|
|
@@ -16,6 +17,7 @@ const generateProps = ({
|
|
|
16
17
|
disabled,
|
|
17
18
|
isLoading,
|
|
18
19
|
label,
|
|
20
|
+
labelId,
|
|
19
21
|
help,
|
|
20
22
|
labelPosition,
|
|
21
23
|
});
|
|
@@ -29,6 +31,7 @@ const Template = (args, { argTypes }) => ({
|
|
|
29
31
|
v-model="value"
|
|
30
32
|
:disabled="disabled"
|
|
31
33
|
:help="help"
|
|
34
|
+
:label-id="labelId"
|
|
32
35
|
:is-loading="isLoading"
|
|
33
36
|
:label="label"
|
|
34
37
|
:label-position="labelPosition"
|
|
@@ -61,7 +61,14 @@ export default {
|
|
|
61
61
|
default: undefined,
|
|
62
62
|
},
|
|
63
63
|
/**
|
|
64
|
-
* The label
|
|
64
|
+
* The id for the label. This id is used by the aria-labelledby attribute on the toggle button.
|
|
65
|
+
*/
|
|
66
|
+
labelId: {
|
|
67
|
+
type: String,
|
|
68
|
+
required: true,
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* The label's position relative to the toggle. If 'hidden', the toggle will add the .gl-sr-only class so the label is still accessible to screen readers.
|
|
65
72
|
*/
|
|
66
73
|
labelPosition: {
|
|
67
74
|
type: String,
|
|
@@ -77,9 +84,6 @@ export default {
|
|
|
77
84
|
icon() {
|
|
78
85
|
return this.value ? 'mobile-issue-close' : 'close';
|
|
79
86
|
},
|
|
80
|
-
shouldShowLabel() {
|
|
81
|
-
return this.label && this.labelPosition !== 'hidden';
|
|
82
|
-
},
|
|
83
87
|
helpId() {
|
|
84
88
|
return this.help ? `toggle-help-${this.uuid}` : undefined;
|
|
85
89
|
},
|
|
@@ -110,38 +114,41 @@ export default {
|
|
|
110
114
|
</script>
|
|
111
115
|
|
|
112
116
|
<template>
|
|
113
|
-
<
|
|
117
|
+
<div
|
|
118
|
+
class="gl-toggle-wrapper gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content"
|
|
119
|
+
:class="{ 'gl-toggle-label-inline': labelPosition === 'left', 'is-disabled': disabled }"
|
|
120
|
+
>
|
|
114
121
|
<span
|
|
115
|
-
|
|
116
|
-
:class="{ 'gl-
|
|
122
|
+
:id="labelId"
|
|
123
|
+
:class="{ 'gl-sr-only': labelPosition === 'hidden' }"
|
|
124
|
+
class="gl-toggle-label"
|
|
125
|
+
data-testid="toggle-label"
|
|
117
126
|
>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<slot name="label">{{ label }}</slot>
|
|
121
|
-
</span>
|
|
122
|
-
<input v-if="name" :name="name" :value="value" type="hidden" />
|
|
123
|
-
<button
|
|
124
|
-
role="switch"
|
|
125
|
-
:aria-checked="isChecked"
|
|
126
|
-
:aria-label="label"
|
|
127
|
-
:aria-describedby="helpId"
|
|
128
|
-
:class="{
|
|
129
|
-
'gl-toggle': true,
|
|
130
|
-
'is-checked': value,
|
|
131
|
-
'is-disabled': disabled,
|
|
132
|
-
}"
|
|
133
|
-
type="button"
|
|
134
|
-
@click.prevent="toggleFeature"
|
|
135
|
-
>
|
|
136
|
-
<gl-loading-icon v-if="isLoading" color="light" class="toggle-loading" />
|
|
137
|
-
<span v-else :class="{ 'toggle-icon': true, disabled: disabled }">
|
|
138
|
-
<gl-icon :name="icon" :size="16" />
|
|
139
|
-
</span>
|
|
140
|
-
</button>
|
|
127
|
+
<!-- @slot The toggle's label. -->
|
|
128
|
+
<slot name="label">{{ label }}</slot>
|
|
141
129
|
</span>
|
|
130
|
+
<input v-if="name" :name="name" :value="value" type="hidden" />
|
|
131
|
+
<button
|
|
132
|
+
role="switch"
|
|
133
|
+
:aria-checked="isChecked"
|
|
134
|
+
:aria-labelledby="labelId"
|
|
135
|
+
:aria-describedby="helpId"
|
|
136
|
+
:class="{
|
|
137
|
+
'gl-toggle': true,
|
|
138
|
+
'is-checked': value,
|
|
139
|
+
'is-disabled': disabled,
|
|
140
|
+
}"
|
|
141
|
+
type="button"
|
|
142
|
+
@click.prevent="toggleFeature"
|
|
143
|
+
>
|
|
144
|
+
<gl-loading-icon v-if="isLoading" color="light" class="toggle-loading" />
|
|
145
|
+
<span v-else :class="{ 'toggle-icon': true, disabled: disabled }">
|
|
146
|
+
<gl-icon :name="icon" :size="16" />
|
|
147
|
+
</span>
|
|
148
|
+
</button>
|
|
142
149
|
<span v-if="help" :id="helpId" class="gl-help-label" data-testid="toggle-help">
|
|
143
150
|
<!-- @slot A help text to be shown below the toggle. -->
|
|
144
151
|
<slot name="help">{{ help }}</slot>
|
|
145
152
|
</span>
|
|
146
|
-
</
|
|
153
|
+
</div>
|
|
147
154
|
</template>
|
package/src/scss/mixins.scss
CHANGED
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
@mixin gl-media-breakpoint-up($name) {
|
|
79
79
|
$min: map-get($breakpoints, $name);
|
|
80
80
|
@if $min == null {
|
|
81
|
-
@error;
|
|
81
|
+
@error "#{$name} is not a valid breakpoint for this @media query.";
|
|
82
82
|
}
|
|
83
83
|
@if $min != 0 {
|
|
84
84
|
@media (min-width: $min) {
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
@mixin gl-media-breakpoint-down($name) {
|
|
104
104
|
$max: map-get($breakpoints, $name);
|
|
105
105
|
@if ($max == null or $max == 0) {
|
|
106
|
-
@error;
|
|
106
|
+
@error "#{$name} is not a valid breakpoint for this @media query.";
|
|
107
107
|
}
|
|
108
108
|
// The maximum value is reduced by 0.02px to work around the limitations of
|
|
109
109
|
// `min-` and `max-` prefixes and with fractional viewport sizes.
|
package/src/scss/utilities.scss
CHANGED
|
@@ -889,6 +889,38 @@
|
|
|
889
889
|
border-style: solid !important;
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
+
.gl-border-dashed {
|
|
893
|
+
border-style: dashed;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.gl-hover-border-dashed:hover {
|
|
897
|
+
border-style: dashed;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.gl-border-dashed\! {
|
|
901
|
+
border-style: dashed !important;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.gl-hover-border-dashed\!:hover {
|
|
905
|
+
border-style: dashed !important;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.gl-border-dotted {
|
|
909
|
+
border-style: dotted;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.gl-hover-border-dotted:hover {
|
|
913
|
+
border-style: dotted;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.gl-border-dotted\! {
|
|
917
|
+
border-style: dotted !important;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.gl-hover-border-dotted\!:hover {
|
|
921
|
+
border-style: dotted !important;
|
|
922
|
+
}
|
|
923
|
+
|
|
892
924
|
.gl-border-t-solid {
|
|
893
925
|
border-top-style: solid;
|
|
894
926
|
}
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
border-style: solid;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
@mixin gl-border-dashed($hover: true) {
|
|
19
|
+
border-style: dashed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@mixin gl-border-dotted($hover: true) {
|
|
23
|
+
border-style: dotted;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
@mixin gl-border-t-solid($hover: true) {
|
|
19
27
|
border-top-style: solid;
|
|
20
28
|
}
|
|
@@ -59,3 +59,17 @@ export const splitAfterSymbols = (symbols, string) => {
|
|
|
59
59
|
|
|
60
60
|
return textParts;
|
|
61
61
|
};
|
|
62
|
+
|
|
63
|
+
export const getAvatarChar = (name) => {
|
|
64
|
+
if (name) {
|
|
65
|
+
// Check if first character is an emjoi
|
|
66
|
+
const match = name.match(/^\p{Emoji}/u);
|
|
67
|
+
if (match) {
|
|
68
|
+
// Return the first match
|
|
69
|
+
return match[0];
|
|
70
|
+
}
|
|
71
|
+
return name.charAt(0).toUpperCase();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return '';
|
|
75
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { splitAfterSymbols } from './string_utils';
|
|
1
|
+
import { splitAfterSymbols, getAvatarChar } from './string_utils';
|
|
2
2
|
|
|
3
3
|
describe('string utils', () => {
|
|
4
4
|
describe('splitAfterSymbols', () => {
|
|
@@ -27,4 +27,22 @@ describe('string utils', () => {
|
|
|
27
27
|
expect(actual.join('')).toEqual(string);
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
describe('Avatar name parsing', () => {
|
|
32
|
+
it('Returns first character of name', () => {
|
|
33
|
+
expect(getAvatarChar('Some Project')).toBe('S');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('Returns empty if name is empty', () => {
|
|
37
|
+
expect(getAvatarChar('')).toBe('');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('Returns emoji if it is first character in name', () => {
|
|
41
|
+
expect(getAvatarChar('🦊Tanuki')).toBe('🦊');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('Returns first character if emoji is not first in name', () => {
|
|
45
|
+
expect(getAvatarChar('tanuki🦊')).toBe('T');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
30
48
|
});
|