resources/lib/codex/mixins/css-icon.less
@import ( reference ) '@wikimedia/codex-icons/codex-icon-paths.less';

//
// To create a CSS-only icon you can do one of the following:
// 1. Apply the .cdx-mixin-css-icon() mixin to an empty <span>, passing in at least the icon param.
//    This method should suffice for any square icon that can exist as a standalone element. This
//    mixin applies all of the other mixins inside this file. See Message.vue for sample usage.
// 2. Apply the individual CSS icon mixins for background, size, alignment, and/or background-image
//    rules to any element. This can be used to apply an icon within another element, like the
//    <select> handle. See Select.vue for sample usage.
//
// These mixins account for icons that vary by reading direction or language.
//

// Get the associated min-size-icon from a size-icon token.
.get-calculated-min-size-icon( @param-size-icon ) {
	// Fallback: when an unrecognized value is passed in, don't crash, but use the same value
	// for size and min-size (T367098). This code runs for all values, but for recognized values
	// it's overridden by one of the lines below.
	@calculated-min-size-icon: @param-size-icon;
}
.get-calculated-min-size-icon( @param-size-icon ) when ( @param-size-icon = @size-icon-medium ) {
	@calculated-min-size-icon: @min-size-icon-medium;
}
.get-calculated-min-size-icon( @param-size-icon ) when ( @param-size-icon = @size-icon-small ) {
	@calculated-min-size-icon: @min-size-icon-small;
}
.get-calculated-min-size-icon( @param-size-icon ) when ( @param-size-icon = @size-icon-x-small ) {
	@calculated-min-size-icon: @min-size-icon-x-small;
}

//
// Get background rules for a CSS icon (or mask rules for icons in buttons).
//
// @param {string} size-icon - The size of the icon, used to set background-size
// @param {string} background-position - The background position value
// @param {boolean} is-button-icon: Whether this icon is inside of a <button> element.
//
.cdx-mixin-css-icon-background( @param-size-icon: @size-icon-medium, @param-background-position: @background-position-base, @param-is-button-icon: false ) {
	.get-calculated-min-size-icon( @param-size-icon );

	// Support Firefox v39-52: fall back to background rules.
	// Support Chrome: Chrome requires the `-webkit` prefix so we must use it in all
	// `@supports` queries.
	@supports not ( ( -webkit-mask-image: none ) or ( mask-image: none ) ) {
		background-position: @param-background-position;
		background-repeat: no-repeat;
		// Set background size to the relative @param-size-icon or to @calculated-min-size-icon,
		// whichever is larger.
		// This ensures that the icon will never appear smaller than @calculated-min-size-icon.
		// Escape the max() call to prevent older Less versions from trying to do the max()
		// calculation at compile time.
		/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
		background-size: calc( ~'max( @{param-size-icon}, @{calculated-min-size-icon} )' );
	}

	@supports ( -webkit-mask-image: none ) or ( mask-image: none ) {
		/* stylelint-disable plugin/no-unsupported-browser-features */
		// Support Chrome: set `-webkit` prefixes explicitly for usage without Autoprefixer.
		// While Codex integrates Autoprefixer, MediaWiki does not.
		-webkit-mask-position: @param-background-position;
		mask-position: @param-background-position;
		// Support Chrome
		-webkit-mask-repeat: no-repeat;
		mask-repeat: no-repeat;
		// Support Chrome
		-webkit-mask-size: calc( ~'max( @{param-size-icon}, @{calculated-min-size-icon} )' );
		mask-size: calc( ~'max( @{param-size-icon}, @{calculated-min-size-icon} )' );
		/* stylelint-enable plugin/no-unsupported-browser-features */
	}
}

//
// Get size styles for a CSS icon.
//
// This sets min-width, min-height, width, and height for a square icon.
//
// @param {string} size-icon: The size of the icon (base, small, x-small, or indicator)
//
.cdx-mixin-css-icon-size( @param-size-icon: @size-icon-medium ) {
	.get-calculated-min-size-icon( @param-size-icon );
	// Set the default icon size.
	min-width: @calculated-min-size-icon;
	min-height: @calculated-min-size-icon;
	// Scale width/height of the span with font size.
	width: @param-size-icon;
	height: @param-size-icon;
}

//
// Get alignment styles for a CSS icon.
//
// @param {string} vertical-align: The vertical-align value
//
.cdx-mixin-css-icon-alignment( @param-vertical-align: text-bottom ) {
	display: inline-block;
	// Vertically align surrounding text in inline, inline-block, and table contexts.
	vertical-align: @param-vertical-align;
}

// Set the color of a mask-image-based icon
//
// @param {hex} fill-color - The fill color of the icon
// @param {boolean} is-button-icon - Whether this icon is inside of a <button> element
.cdx-mixin-css-icon-mask-image-color( @param-fill-color, @param-is-button-icon ) {
	// For icons outside of buttons, use background-color to set the icon color.
	& when not ( @param-is-button-icon ) {
		background-color: @param-fill-color;
	}
	// For icons within buttons, set transition rules. The icon color will be changed in the
	// Button component depending on props and state.
	& when ( @param-is-button-icon ) {
		transition-property: @transition-property-icon-css-only;
		transition-duration: @transition-duration-base;
	}
}

//
// Set the icon image, either via mask-image or background-image.
//
// For browsers that support mask-image, this mixin will apply the background image as a mask, so
// that background-color rules in the Button styles can set the icon color.
//
// For browsers that don't support mask-image, this mixin sets the icon as a background image with
// the color set via opacity.
//
// @param {string} icon - The icon to show
// @param {hex} fill-color - The fill color of the icon (defaults to @color-base)
// @param {boolean} is-button-icon - Whether this icon is inside of a <button> element
//
.cdx-mixin-css-icon-try-mask-image( @param-icon, @param-fill-color, @param-is-button-icon ) {
	@hex-color-black: #000;
	@escaped-color-black: escape( @hex-color-black );
	@image-url: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="@{escaped-color-black}">@{param-icon}</svg>';

	// Support Firefox v39-52: fall back to background-image.
	// Support Chrome: Chrome requires the `-webkit` prefix so we must use it in all
	// `@supports` queries.
	@supports not ( ( -webkit-mask-image: none ) or ( mask-image: none ) ) {
		background-image: url( @image-url );
		// Set icon color to white in night mode.
		filter: invert( @filter-invert-icon );
		// Set icon color closer to @color-base.
		opacity: @opacity-icon-base;

		// Fallback for @color-inverted icons within buttons.
		& when ( @param-is-button-icon ) {
			.cdx-button:not( .cdx-button--weight-quiet ):disabled &,
			.cdx-button--weight-primary.cdx-button--action-progressive &,
			.cdx-button--weight-primary.cdx-button--action-destructive & {
				// Set icon color close to @color-inverted.
				filter: invert( @filter-invert-primary-button-icon );
			}
		}
	}

	// For browsers that support it, use mask-image to set the SVG so we can change the color with
	// much more granularity.
	@supports ( -webkit-mask-image: none ) or ( mask-image: none ) {
		// Support Chrome
		/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
		-webkit-mask-image: url( @image-url );
		/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
		mask-image: url( @image-url );

		// Set background-color to color the icon
		.cdx-mixin-css-icon-mask-image-color( @param-fill-color, @param-is-button-icon );
	}
}

//
// Get rules to set the appropriate image for the icon.
//
// Note that in RTL contexts, this mixin requires `dir="rtl"` either on the icon element itself
// or on the <html> element.
//
// This mixin takes in an icon, which is really a Less variable generated by the codex-icons
// package. These variables are lists of icon data that contain:
// 1. The default icon path (a string)
// 2. Whether the icon should flip in RTL ('true' or 'false')
// 3. Exceptions to the flip rule ('false' or a selector string that will rule out languages)
// 4. RTL-specific icon path ('false' or the path string)
// 5. Whether the icon has language-specific variants ('true' or 'false')
// 6+ If there are language-specific variants, they will be included as string pairs after the other
//   icon data. The first item in the pair is a lang code and the second is the icon path for that
//   language.
//
// @param {string} icon - The icon to show (follows the pattern @cdx-icon-icon-name, e.g. @cdx-icon-info-filled )
// @param {hex} fill-color - The fill color of the icon (defaults to @color-base)
// @param {boolean} is-button-icon - Whether this icon is inside of a <button> element
//
.cdx-mixin-css-icon-background-image( @param-icon, @param-fill-color: @color-base, @param-is-button-icon: false ) {
	// Extract icon data from the list.
	@default-icon: extract( @param-icon, 1 );
	@should-flip: extract( @param-icon, 2 );
	@flip-exceptions: extract( @param-icon, 3 );
	@rtl-icon: extract( @param-icon, 4 );
	@has-lang-variants: extract( @param-icon, 5 );

	// Add default image.
	.cdx-mixin-css-icon-try-mask-image( @default-icon, @param-fill-color, @param-is-button-icon );

	// Flip icons with no shouldFlip exceptions.
	& when ( @should-flip = 'true' ) and ( @flip-exceptions = 'false' ) {
		&[ dir='rtl' ],
		html[ dir='rtl' ] &:not( [ dir='ltr' ] ) {
			transform: scaleX( -1 );
		}
	}

	// Flip icons with shouldFlip exceptions.
	& when ( @should-flip = 'true' ) and not ( @flip-exceptions = 'false' ) {
		// Create a selector string out of each exception lang code.
		// Final selector will look like `:not( :lang( he ) ):not( :lang( yi ) )`
		@exceptions-selector: e( replace( @flip-exceptions, '(^| )([^ ]+)', ':not( :lang( $2 ) )', 'g' ) );

		&[ dir='rtl' ],
		html[ dir='rtl' ] &:not( [ dir='ltr' ] ) {
			&@{exceptions-selector} {
				transform: scaleX( -1 );
			}
		}
	}

	// If an icon has an RTL-specific icon, apply it.
	& when not ( @rtl-icon = 'false' ) {
		&[ dir='rtl' ],
		html[ dir='rtl' ] &:not( [ dir='ltr' ] ) {
			.cdx-mixin-css-icon-try-mask-image( @rtl-icon, @param-fill-color, @param-is-button-icon );
		}
	}

	// Set language-specific icons.
	& when ( @has-lang-variants = 'true' ) {
		@icon-list-length: length( @param-icon );

		// Language-specific icons are represented by list items in @param-icon. They consist of a
		// lang code, e.g. ar, and an icon path.
		// Since we can't use modern Less features in MediaWiki, we need a recursive mixin.
		.get-lang-rules( @i: 6 ) when ( @i <= @icon-list-length ) {
			@lang-data: extract( @param-icon, @i );
			@lang-code: extract( @lang-data, 1 );
			@lang-icon: extract( @lang-data, 2 );

			&:lang( @{lang-code} ) {
				.cdx-mixin-css-icon-try-mask-image( @lang-icon, @param-fill-color, @param-is-button-icon );
			}
			.get-lang-rules( @i + 1 );
		}

		.get-lang-rules();
	}
}

//
// Create a square, standalone CSS icon.
//
// This mixin only supports icons provided by Codex, which will be embedded as data URLs. To provide
// a custom icon (either as a data URL or as a regular URL), set @param-icon to 'none', and set
// `mask-image` and `-webkit-mask-image` to the URL of the custom icon.
//
// @param {string} icon - The icon to show (follows the pattern @cdx-icon-icon-name, e.g. @cdx-icon-info-filled), or 'none'
// @param {hex} fill-color - The fill color of the icon
// @param {string} size-icon: The size of the icon
// @param {boolean} is-button-icon: Whether this icon is inside of a <button> element.
// @param {string} background-position - The background position value
// @param {string} vertical-align: The vertical-align value
//
/* stylelint-disable @stylistic/indentation */
.cdx-mixin-css-icon(
	@param-icon,
	@param-fill-color: @color-base,
	@param-size-icon: @size-icon-medium,
	@param-is-button-icon: false,
	@param-background-position: @background-position-base,
	@param-vertical-align: text-bottom
) {
/* stylelint-enable @stylistic/indentation */
	.cdx-mixin-css-icon-background( @param-size-icon, @param-background-position, @param-is-button-icon );
	.cdx-mixin-css-icon-size( @param-size-icon );
	.cdx-mixin-css-icon-alignment( @param-vertical-align );

	& when not ( @param-icon = 'none' ) {
		.cdx-mixin-css-icon-background-image( @param-icon, @param-fill-color, @param-is-button-icon );
	}

	& when ( @param-icon = 'none' ) {
		// The caller is going to set mask-image themselves; but we still need to set the color
		.cdx-mixin-css-icon-mask-image-color( @param-fill-color, @param-is-button-icon );
	}
}
