Skip to content

Commit

Permalink
Fix revert toggle partial rendering
Browse files Browse the repository at this point in the history
This commits fixes an issue where the `REVERT` label on revert toggle
might render as `REVER` or in a similarly clipped manner due to its
fixed width. The problem is visible when certain fonts fail to load or
browser engines render content non-standardly.

Changes:
- Refactor UI component to have its own separate Vue component with unit
  tests.
- Rework component design to utilize flexbox, enhancing its adaptability
  and simplifying the structure.
- Remove obselete `webkit` directives.
- Refactor SCSS for clearer structure and better SCSS best-practices.
- Use `em` when possible instead of `px` for improved responsiveness.
  • Loading branch information
undergroundwires committed Aug 14, 2023
1 parent bc91237 commit 39e650c
Show file tree
Hide file tree
Showing 4 changed files with 509 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
<template>
<div class="checkbox-switch">
<input
type="checkbox"
class="input-checkbox"
v-model="isReverted"
@change="toggleRevert()"
v-on:click.stop>
<div class="checkbox-animate">
<span class="checkbox-off">revert</span>
<span class="checkbox-on">revert</span>
</div>
</div>
<ToggleSwitch
v-model="isChecked"
:stopClickPropagation="true"
:label="'revert'"
/>
</template>

<script lang="ts">
import {
PropType, defineComponent, ref, watch,
computed,
} from 'vue';
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IReverter } from './Reverter/IReverter';
import { INodeContent } from './INodeContent';
import { getReverter } from './Reverter/ReverterFactory';
import ToggleSwitch from './ToggleSwitch.vue';
export default defineComponent({
components: {
ToggleSwitch,
},
props: {
node: {
type: Object as PropType<INodeContent>,
Expand All @@ -46,138 +44,43 @@ export default defineComponent({
);
onStateChange((newState) => {
updateStatus(newState.selection.selectedScripts);
updateRevertStatusFromState(newState.selection.selectedScripts);
events.unsubscribeAll();
events.register(newState.selection.changed.on((scripts) => updateStatus(scripts)));
events.register(
newState.selection.changed.on((scripts) => updateRevertStatusFromState(scripts)),
);
}, { immediate: true });
async function onNodeChanged(node: INodeContent) {
handler = getReverter(node, currentState.value.collection);
updateStatus(currentState.value.selection.selectedScripts);
updateRevertStatusFromState(currentState.value.selection.selectedScripts);
}
async function updateRevertStatusFromState(scripts: ReadonlyArray<SelectedScript>) {
isReverted.value = handler?.getState(scripts) ?? false;
}
function toggleRevert() {
function syncReversionStatusWithState(value: boolean) {
if (value === isReverted.value) {
return;
}
modifyCurrentState((state) => {
handler.selectWithRevertState(isReverted.value, state.selection);
handler.selectWithRevertState(value, state.selection);
});
}
async function updateStatus(scripts: ReadonlyArray<SelectedScript>) {
isReverted.value = handler?.getState(scripts) ?? false;
}
const isChecked = computed({
get() {
return isReverted.value;
},
set: (value: boolean) => {
syncReversionStatusWithState(value);
},
});
return {
isReverted,
toggleRevert,
isChecked,
};
},
});
</script>

<style scoped lang="scss">
@use 'sass:math';
@use "@/presentation/assets/styles/main" as *;
$color-bullet-unchecked : $color-primary-darker;
$color-bullet-checked : $color-on-secondary;
$color-text-unchecked : $color-on-primary;
$color-text-checked : $color-on-secondary;
$color-bg-unchecked : $color-primary;
$color-bg-checked : $color-secondary;
$size-width : 85px;
$size-height : 30px;
// https://www.designlabthemes.com/css-toggle-switch/
.checkbox-switch {
display: inline-block;
overflow: hidden;
position: relative;
width: $size-width;
height: $size-height;
-webkit-border-radius: $size-height;
border-radius: $size-height;
line-height: $size-height;
font-size: math.div($size-height, 2);
input.input-checkbox {
position: absolute;
left: 0;
top: 0;
width: $size-width;
height: $size-height;
padding: 0;
margin: 0;
opacity: 0;
z-index: 2;
@include clickable;
}
.checkbox-animate {
position: relative;
width: $size-width;
height: $size-height;
background-color: $color-bg-unchecked;
-webkit-transition: background-color 0.25s ease-out 0s;
transition: background-color 0.25s ease-out 0s;
// Circle
&:before {
$circle-size: $size-height * 0.66;
content: "";
display: block;
position: absolute;
width: $circle-size;
height: $circle-size;
border-radius: $circle-size * 2;
-webkit-border-radius: $circle-size * 2;
background-color: $color-bullet-unchecked;
top: $size-height * 0.16;
left: $size-width * 0.05;
-webkit-transition: left 0.3s ease-out 0s;
transition: left 0.3s ease-out 0s;
z-index: 10;
}
}
input.input-checkbox:checked {
+ .checkbox-animate {
background-color: $color-bg-checked;
}
+ .checkbox-animate:before {
left: ($size-width - math.div($size-width, 3.5));
background-color: $color-bullet-checked;
}
+ .checkbox-animate .checkbox-off {
display: none;
opacity: 0;
}
+ .checkbox-animate .checkbox-on {
display: block;
opacity: 1;
}
}
.checkbox-off, .checkbox-on {
text-transform: uppercase;
float: left;
font-weight: 700;
-webkit-transition: all 0.3s ease-out 0s;
transition: all 0.3s ease-out 0s;
}
.checkbox-off {
margin-left: math.div($size-width, 3);
opacity: 1;
color: $color-text-unchecked;
}
.checkbox-on {
display: none;
float: right;
margin-right: math.div($size-width, 3);
opacity: 0;
color: $color-text-checked;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<template>
<div
class="toggle-switch"
@click="handleClickPropagation"
>
<input
type="checkbox"
class="toggle-input"
v-model="isChecked"
>
<div class="toggle-animation">
<span class="label-off">{{ label }}</span>
<span class="label-on">{{ label }}</span>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue';
export default defineComponent({
props: {
value: Boolean,
label: {
type: String,
required: true,
},
stopClickPropagation: {
type: Boolean,
default: false,
},
},
emits: {
/* eslint-disable @typescript-eslint/no-unused-vars */
input: (isChecked: boolean) => true,
/* eslint-enable @typescript-eslint/no-unused-vars */
},
setup(props, { emit }) {
const isChecked = computed({
get() {
return props.value;
},
set(value: boolean) {
if (value === props.value) {
return;
}
emit('input', value);
},
});
function handleClickPropagation(event: Event): void {
if (props.stopClickPropagation) {
event.stopPropagation();
}
}
return {
isChecked,
handleClickPropagation,
};
},
});
</script>

<style scoped lang="scss">
@use 'sass:math';
@use "@/presentation/assets/styles/main" as *;
$color-toggle-unchecked : $color-primary-darker;
$color-toggle-checked : $color-on-secondary;
$color-text-unchecked : $color-on-primary;
$color-text-checked : $color-on-secondary;
$color-bg-unchecked : $color-primary;
$color-bg-checked : $color-secondary;
$size-height : 30px;
$size-circle : math.div($size-height * 2, 3);
$padding-horizontal : 0.40em;
$gap : 0.25em;
@mixin locateNearCircle($direction: 'left') {
$circle-width: calc(#{$size-circle} + #{$padding-horizontal});
$circle-space: calc(#{$circle-width} + #{$gap});
@if $direction == 'left' {
margin-left: $circle-space;
} @else {
margin-right: $circle-space;
}
}
@mixin setVisibility($isVisible: true) {
@if $isVisible {
display: block;
opacity: 1;
} @else {
display: none;
opacity: 0;
}
}
.toggle-switch {
display: flex;
overflow: hidden;
position: relative;
width: auto;
height: $size-height;
border-radius: $size-height;
line-height: $size-height;
font-size: math.div($size-height, 2);
input.toggle-input {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
z-index: 2;
@include clickable;
}
.toggle-animation {
display: flex;
align-items: center;
gap: $gap;
width: 100%;
height: 100%;
background-color: $color-bg-unchecked;
transition: background-color 0.25s ease-out;
&:before {
content: "";
display: block;
position: absolute;
left: $padding-horizontal;
$initial-top: 50%;
$centered-top-offset: math.div($size-circle, 2);
$centered-top: calc(#{$initial-top} - #{$centered-top-offset});
top: $centered-top;
width: $size-circle;
height: $size-circle;
border-radius: 50%;
background-color: $color-toggle-unchecked;
transition: left 0.3s ease-out;
z-index: 10;
}
}
input.toggle-input:checked + .toggle-animation {
background-color: $color-bg-checked;
flex-direction: row-reverse;
&:before {
$left-offset: calc(100% - #{$size-circle});
$padded-left-offset: calc(#{$left-offset} - #{$padding-horizontal});
left: $padded-left-offset;
background-color: $color-toggle-checked;
}
.label-off {
@include setVisibility(false);
}
.label-on {
@include setVisibility(true);
}
}
.label-off, .label-on {
text-transform: uppercase;
font-weight: 700;
transition: all 0.3s ease-out;
}
.label-off {
@include setVisibility(true);
@include locateNearCircle('left');
padding-right: $padding-horizontal;
}
.label-on {
@include setVisibility(false);
color: $color-text-checked;
@include locateNearCircle('right');
padding-left: $padding-horizontal;
}
}
</style>
Loading

0 comments on commit 39e650c

Please sign in to comment.